Author: Bär D.  

Tags: programmierung  

Text
                    programmiert Txx
b a Sie t wasia n da rjhF. a ch bu c hM

Vorwort Als ich vor zwei Wochen meinen neuen Freund Schrödinger in meinem Stamm- lokal bei einem Bier kennenlernte, hat er mir gesagt, er müsse für seinen künf- 1 eigen Arbeitgeber €♦♦ programmieren lernen. Bisher hat er seine Programme noch in altem Basic geschrieben. Nun Ja, ich habe ihm natürlich daraufhin gesagt, dass er damit nicht mehr ganz auf der Höhe derzeit ist. Da ich selbst als Programmierer tätig bin. bat mich mein Freund nach dem dritten Bier, ob Ich ihm nicht C++ .etwas anders* bei- bringen könne! Ich habe mir das über- legt, und nach dem vierten Bier und einem vollen Aschenbecher habe Ich ihm schließlich versprochen, ihm zu helfen. Ich sagte ihm, dass er nicht erwarten dürfe, von heute auf mor- gen perfekt C++ zu können. Das braucht schon etwas mehr Zelt und vor allem Erfahrung. Aber ich kann ihm zumindest einen etwas einfacheren und entspannteren Einstieg geben. Am nächs- ten Tag. nach ein paar Aspirin, habe ich mir mal notiert, was er so alles grundlegend für seine ersten Schritte in die C++-Wdt gebrauchen kann, ohne ihm gleich ein ganzes Kompendium mit Informationen an den Kopf zu werfen. Er hat mir nämlich verraten, dass er schnell die Lust an etwas verliert, wenn er nur trockene Theorie und Tabellen durcharbeiten muss, Im Nach- hinein ist mir jetzt schon klar, dass cs nicht leicht sein wird, Schrödinger bei Laune zu halten. Aber ich gebe mein Bestes, versprochen? Vorwort 23
Erschwert wurde mir diese Aufgabe, da während des Schrei- bens auch noch der C++11 -Standard eingeführt wurde, aber einige seiner Compiler den Spaß nicht mit machen wollten, weil es eben immer dauern kann, bis der Standard überall komplett drin ist. Daher habe ich mich entschlossen, ihm innerhalb des Buches einige Notizen zum C++11 Standard zu hinterlassen und am Ende etwas ausführlicher darüber zu berichten, Und für den Falt der Fälle, dass Schrödinger mal wieder alles falsch abtippt oder sogar zu faul zum Tippen Ist, habe ich ihm sämtliche Listings auf der Webseite zur Verfügung gestellt {www.galileo-press.de/2B53 und von da aus in der linken Navigation zur Bonus-Seite). Die abgedruckten Lis- tings hier sind häufig etwas gekürzt, weil ich davon ausge- he, dass Schrödinger selbst in der Lage ist, das Programm zu komplettieren. Aber eben für den Fall der Fälle weiß er ja jetzt, wo er ein Beispiel finden kann. Viel Spaß beim Lesen wünscht Dieter Bär An dieser Stelle mochte ich mich außerdem noch bei meinem finnischen Freund Esa Holopainen http://www.verikoirat.com/ english/) bedanken, der mir für Schrödin- ger zur Aufheiterung tolle Karikaturen zur Verfügung gestellt hat. Ebenfalls bedanken möchte ich mich bei meinem amerikani- schen Freund Daniel McQuillen (http:// www.sintplediagrams.coni/) für die tolle Software SimpleDiagrams und seinen netten Support dazu. 24 Vorwort
Entwicklungs- Umgebungen für C++ Mr richten unsbin... Schrödinger findet hernus, dass er zur Entwicklung von C++-Programmen einen Compiler benötigt. Seine künftigen Kollegen haben Ihm gesagt, dass er hierzu zunächst verwenden kann, wozu er Lust hat. Keine leichte Aufgabe für Schrödinger, sich Im Dschungel von Compilern mit und ohne grafischer Oberfläche zurechtzufinden. Schrödinger hat sich natürlich vorbereitet und keine Kosten und Mühen gescheut, sich seinen Arbeitsplatz elnzurichtan. Er hat sich gleich drei Rechner zugelegt, um auf Num- mer sicher zu gehen und alle Systeme abzudecken: einen Rechner mit Windows, eine Maschine mit Linux und natürlich einen Mac.
Okay, bevor du überhaupt anfangen kannst, deine Programme zu schreiben, brauchst du natürlich ein Werkzeug, um einen lesbaren Quellcode in einen nicht mehr lesbaren Maschinenkode zu übersetzen. Für solche Zwecke benötigst du in €*♦ einen Compiler. Solange du den C++-Standard verwendest, ist dein Qucllcode auf dir gängigsten Betriebssysteme portierbar. Wenn du den lesbaren Quellcode aber in einen Maschinen code übersetzt hast, dann kannst du diesen nur noch auf dem entsprechenden Betriebs- system ausführen. Damit sollte dir klar sein, dass der übersetzte Maschinencode ent- weder auf deinem Windows-Rechner oder auf dem Linux-System oder auf dem Mac läuft und nicht mehr portabel ist wie der Quellcode, (Hinttrfrundin^l Genau genommen wird der Quellcode von einem Compiler in eine Objektdatei (*.obj oder ’.o) übersetzt. Diese Objektdatei(en) wiederum wird/werden dann von einem Linker zu einer ausführbaren Datei gebunden. Aber häufig ist bei der Rede von einem Compiler auch gleichzeitig der Linker als komplette Einheit mit gemeint. Aber du solltest trotzdem wissen, dass es sich hierbei um zwei verschiedene Dinge handelt. - I Cw otykul«t«i Unfcir L StftrUtlM- il.biVcrtfcfk AujAihrtart । Vereinfachte Darstellung. w*e man aus einer OvHkfatei eine luiiuhr- b*re Ditea flucht 21
Um Jem aus deinem Quellcode ein echtes Programm (eine ausführbare Datei) zu machen, brauchst du im Grunde nur einen ASCII-Texteditor, in den du deinen Quelltext eintippst, und dann eben einen Compiler (mit Linker) zum Übersetzen des Quelltextes, um daraus ein Programm zu machen, Natürlich kannst du hierbei auch ein AlIes-drin-Kümplettpakct mit einer Entwicklungs- umgebung verwenden. Eniwicklungsumgebungen haben den Vorteil, dass eben alles gleich an Bord ist. Hier findest du den Editor, Compiler, Linker und noch viele weitere Dinge wie Debugger, Projektverwaltung, Proiller und noch einiges mehr vor. Der einzige Nachteil von solchen Alles-drin-Komplettsachen ist halt, dass hierfür ein wenig mehr Einarbeitungszeit nötig wird. Wie dem auch sei, ich bin hier nicht dafür da, für bestimmte Compilerhersteller zu werben, sondern ich will dir lediglich einen kleinen Überblick zu diesem Markt ver- schaffen. Letztendlich sind alle nur ein Mittel zum Zweck mit demselben Ziel, nämlich ein ausführbares Programm zu erzeugen. Es gibt komplette Entwicklungsumgebungen, die für alle gängigen Systeme erhältlich sind und unter Windows, Linux (und Unix-like) sowie Mac OS laufen. Zwei ganz große und umfangreiche Projekte sind die IDEs Edipse und NetBeans. Beide Entwicklungs- Umgebungen wurden in und für Java geschrieben. Aber trotzdem bieten diese auch eine hervorragende Unterstützung anderer Programmiersprachen an, wie u.a. natürlich auch für C**, (nt toi c* 3 wr.93uBgtfcuF.9en für 27
>Axh (chpic tolle ituUvnj jn Ebenfalls für fast alle Systeme vorhanden ist das Qt SDK von Nokia (worin die IDE Qt Creator IDE enthalten ist), womit sich neben tollen Anwendungen mit grafischer Oberfläche natürlich auch einfache C++-Programnte erstellen lassen. Auch eine sehr interessante Oberfläche stellt Code::Blocks zur Verfügung, die mittlerweile auch auf Windows. Linux und Mac OS erhältlich Ist. (AbUge) Alle hier erwähnten multikulturellen Sachen sind umsonst erhältlich und kosten dich keinen Cent?!! Dominierend auf den WindowsRechnern dürften wohl die haus- eigenen Produkte sein. Microsoft bietet hierbei unter der Haus- marke Visual C++ Studio mehrere kommerzielle Versionen an. Aber es gibt mit Visual C++ Express auch eine kostenlose Version aus dem Hause dieser Entwicklungsumgebting. welche dir für den normalen Einstieg oder Umstieg in die C++ Welt vorerst völlig ausreichen dürfte. 28 cipu«! ctxs
Mkfowh ViMMbl im Etecati Auch sehr beliebt ist der Kommandozeilen-Compiler MinGW-Compiler, der eine Portierung des GCC Compilers auf Windows ist. Darauf au (bauend werden viele andere tntwkklungsumgebungen verwendet. Früher war auch Borlands C++Bultder sehr beliebt, der allerdings jetzt nur noch C++Buitdcr heißt, weil er nicht mehr Borland gehört. Auf Linux oder anderen un(ix)artigen Systemen kommt im Grunde fast immer das GCC-Paket als Compiler zum Einsatz. Darauf greifen natürlich die meisten IDEs zu. Beliebte Em- Wicklungsumgebungen sind hier z. B. KDevelop und Anjuta. Es gibt aber mittlerweile auch viele kleinere interessante IDEs. (Hantrrgrundinfal GCC, gcc und g++! Alles dasselbe? Nein, nicht ganz. GCC ist die GNU Compiler Collection (also eine Sammlung von Compilern}, gcc (klcingeschrieben) ist der C-Compiler der Sammlung, und g++ ist (ja, richtig) der C++-Compiler von GCC! Cntuic*Jun93uag«bun9*n für 29
AnjuU in «io» »ehr j/igenehm« Emwichfungv umg*bung (hier unter Ubuwitu Linwa). Auch beim Apfel setze nun nach wie vor auf GCC, Aber hier sind erste Anzeichen eines Umbruchs zu erkennen. Apple hat schon einige Euros für Clang lockergcmaiht. Clang ist ein Cninpiler-Fronlend für C, C++, Objective-C und -C++. So darf man davon ausgehen, dass Clang über kurz oder lang den GCC auf dem Mac ersetzen wird. Die wohl am meisten eingesetzte Entwickln ngsumgebung auf dem MacOS dürfte ganz klar das hauseigene Produkt Xcode sein. + <•? - KrteWtta 10 4 J tjvwrwr* ► __ ► Ö®**»******** ► trodxii. ► (HuUMS ► u>fr»v» ▼ iVMMi ► iSiMfc**** ► jfl| hwgww Nh* ► iBi r «n j -iciüvw- «Birg roMHt-X« tt-?; UM «*A <) < // i»Mt CO* »WV... ooMC «i *wiit4 Wit'* « «mm; r**XA». «| > 0»bv«r «r **d<d Jtxcf+öK H**on(f<n bc-he-bt auf dem Mac ii< Xcode. 30 C4Ptt*l [IMS
Lass uns endlich loslegen... Ja, cs gibt wirklich eine gewaltige Menge an (kostenlosen) Compilern bzw. Entwicklung! Umgebungen, und du hast die Qual der Wahl dabei. Welchen Compiler oder welche Entwicklungsumgebung du hierbei verwendest, hangt natürlich zum einen vom System und zum anderen vom persönlichen Geschmack ab. Da du ja gleich für alle drei Systeme vorbereitet bist, hast du noch mehr Auswahl zur Verfügung. Du musst nicht zwangsläufig wie Schrödinger lauter neue Rechner anschaf- fen, um deinen Code oder deine Beispiele eventuell auf anderen Betriebssystemen oder Compilern zu testen. Hierzu reicht häufig auch eine Virtualisierung aus. So kannst du bspw. auf dem Mac-Rechner über eine Virtualisierungssoftware Win- dows oder Linux .installieren**. Persönlich habe ich gute Erfahrungen mit Parallels Desktop gemacht. Auf Windows oder Linux wiederum verwende ich gerne VMWare, um ein anderes Betriebssystem als Gast zu haben. Ein Kollege von mir (unter Ubuntu Linux) schwort wiederum total auf das kostenlose „VirtualBox SE" (OpenSou ree), was auch für Windows erhältlich ist. Übersetzen mit einer Entwicklungsumgebung Hier möchte ich dir mm eine kleine Anleitung schreiben, wie du aus einem einfachen Quclltext ein ausführbares Programm mit einer Entwfcklungsumgebung erstellen kannst. Irn Beispiel wird davon ausgegangen, dass du die Entwickhingsumgebiing bereits heruntergeladen und installiert hast. Die Anleitung Ist natürlich sehr vereinfacht, und hier gilt, dass du dich schon CntuicXlun9iuag»0un9«n für 31
selbst ein wenig mit deiner Entwicklungsumgebung befassen musst. In der Abbildung wurde zwar Visual C++ Express dafür verwendet, aber der Vorgang ist bei den meisten anderen Entwicklungsuingebungen auch sehr ähnlich aulgebaul. Nur dass hier und da der Befehl etwas anders lautet und sich woanders befindet. Ain einfachsten dürfte cs zunächst immer sein, wenn du ein neues Projekt startest. Hier eben über Datei • Neu • Projekt. Erster Schrill durfte immer Win, em ntufi Projekt ru starten. Gewöhnlich hiHt dn pHii ein Wiiird weiter, die Art deines ProjeMes- IttliorwAhltfi. Als Nächstes erscheint meistens ein Dialog, der dir dabei hilft, zu entscheiden, was ftir eine Art von Projekt du erstellen willst, wie du dein Projekt nennen und wo du cs abspeichem willst. Hier ist cs oft gut, ein leeres Projekt oder eine C4*-Konsolenanwendung zu erstellen. 32 C4okt«i eins
Abhängig von der Entwicklungsumgebung findest du jetzt häufig schon einen vordefinierten leeren Quellcode mit seinem Grundgerüst vor. Bel anderen Entwicklungsumgebungen musst du erst noch ein neues F.lement/cinc neue Datei hinzu fügen. Neue Elemente wirst du so oder so irgendwann hinzufügen müssen, wenn du deinen Quellcode in mehrere Quell- oder Headerdateien auf- teilst. Achte darauf, dass du den Quellcodc auch zum Projekt hinzu fügst und nicht nur einfach eine neue Datei anlegstI Neue ttenrven /um Projekt häntvliigen. Auch hier hilft dir häufig ein weiterer Dialog bei der Wahi, was für eine Art von Datei du zum Projekt hinzufügen willst. Hier ist es eine Quelldatci mit der Endung **Cpp. Aber bei späteren Projekten wirst du auch des öfteren mal eine Headerdatei mit der Endung * .h hinzufügen, Meistem gibt es Auch einen Wi»<rd. der dir hiHt, eiM-pHiemle Ditel hinrumfuxrn. Cntwic*;3un93uag»toUFi9«-n für <*• 33
5 Hast du deine Daici(en) hinzugefügt, kannst du anfangen, deinen Quelltext ein- zulippcn. Nach dem Abspeichern kannst du dann den Quellcode übersetzen (kompilieren) und anschließend ausführen. Reim VC++ kannst du den Quelltext über Erstellen • Projektmappe erstellen und Erstellen ♦ Kompilieren über- setzen und dann mit Debuggen • Starten ohne Debugging ausführen. I»U« rturden Quelltti' einlippen, überleiten und dirtrt dM Prftgrunm i tuten. JFehkd Wenn du beim Einstieg hier verzweifelst und wirklich nicht klarkommst, kannst du ja eine E-Mail an Schrödinger senden, der da ziemlich gut durchblickt: schroedinger@dieter-baer.de 34 upini cixs
g++ und clang++ Die Verwendung von Kommandozeilen-Compllem wieg** oder clang++ erscheint dir vielleicht zunächst etwas altbacken, aber hat natürlich den Vorteil, dass du dich nicht mit einem Monster von Programm mit unzähligen Funktionen herumschlagen musst, im Grunde musst du lediglich deinen Quelltext mit einem beliebigen ASClI-Editor schreiben, abspeichern und diesen dann mit einem Kommando übersetzen. Gerade für .kleinere Projekte* oder Listings im Buch reicht dies völlig aus. Zudem kommt hinzu, dass bei den meisten Linux- und Unlx-Systemen der GCC (und somit g++) von Haus aus bereits installiert ist oder gegebenenfalls ganz schnell nachinstailien werden kann. •* _______________Mrirx rixlen Grhvrn.uch< Schroedinger S g++ -o main main.cpp Schroedinger $ ,/eoin Juchu, es läuft,,.! Schroedinger $ g++ -Wall -pedantic -o main main.epp Z Schroedinger $ ,/nain Juchu, es läuft,,.! * * Schroedinger S Natürlich gehe ich auch davon aus. dass du weißt, was eine Kommandozeile ist und wie du dich mit unterschiedlichen Kommandos dort durch die Verzeichnisse . j />* _Tj _r hangelst. Schließlich willst du ja Pro- grammierer lernen und hier keine Ein- Führung haben, wie du deinen Rechner bedienen kannst. jU Wenn du deinen Quelltext geschrieben und gespeichert hast, brauchst du nur noch in das Verzeichnis zu wechseln, in dem du den Quelltcxt gespeichert hast. den Compiler anzuwerfen und deinen Code übersetzen zu lassen. (ntui c* 3 wrujiu «g für 35
Hier habe ich beim ersten Obersetzungsvorgang den Schalter -o verwendet, so dass der Compiler aus der Quelldatci main.epp die ausführbare Datei main macht. Du kannst natürlich auch einen anderen Programm- namen für main verwenden. Beim zweiten Übersetzungsvorgang habe ich ein paar Schalter mehr verwendet, die mir mehr Informationen über diverse Warnungen zurückgeben, die sonst nicht angezeigt würden. -Wall gibt z. B. sinnvolle Warnungen vom Compiler aus und -pedant LC gibt Warnungen aus, die vom ANSI<«*'Standard gefordert werden. Willst du deinen Quellcode .nur“ kompilieren, also eine Objektdatei daraus machen, brauchst du nur den Schalter -C zu verwenden. Es gibt natürlich eine gewaltige Anzahl weiterer Schalter, die du hierbei verwenden kannst. Aber auch hierzu empfehle ich dir, dich bei Bedarfselberein wenig einzulesen. Vergiss nicht, dass du hier ein C++-Buch vor dir hast, kein Buch über die Verwendung von speziellen Compilern' 31
am Enda läuft es So, an der Auswahl von Compilern bzw. Entwicklungsumgebungen mangelt es ja nun wirklich nicht. Daher will ich dir hier noch ein paar Compiler bzw. Eniwicklungs* Umgebungen auflisten, damit du einen Oberblick hast. Du kannst diese gerne testen und selber sehen, was dir persönlich am meisten zusagt. Compiler Kostenlos ] Link System [ IDE 1 GCC j‘ http7/g« gnu.cfg/ Win, Linux, Mac, Unlx-Wke nein Clans j» httpS/cteog ttvm.öfg/ Unixdike, Mac nein Microsoft Visual C++ nein /ittpy/www.nifcroio/r cpm/ germjny/wlutlltudiö/ Windows P Microsoft Visual Studio i» hUp7/wwvt’.microtofi.com/ Windows J* Express gemMy/expmsS NctBcans htlpMielbrini off/ platlformurublungij M Edipse Ja httpS/wvrwtchpicwg/ platt1ormunj.bhingig i» Qt Creator IDE M httpY/qt nokia.com/ Win, Linux, Mac Code::Blocks J» httpY/yv^wx&ckb/ocks tfg/ Win, Linux, Mac i* C++Buildcr XE2 nein h t tp/7www. rmfejirc adero xom/ ptöductl/ctwMcf Win. Mac p KOevdop ja httpS/hfevdopOfg/ Linux/Unix-Iike, Mac, Win p Afijuta J* h t tpy/www. anju t a. org/ Linux, BSD M XCOde nein http7Aftvtioptf tpptexom/ tedrnofogif^L/tooii/ Mac OS X MinGW M http7/iyww.mtngw.org/ Windows nein MinGW Developer Studio i» http7/l(Ht ^b^et.fi/^tec/ mtngwittfdio php Windows >« Dev-C++ >* h t tp //www. b tixxfahcd ntt/ dewpp.faml Windows M lr»tei-C++ nein httpV'/ioftWÄrv intxf.com/ M-u^lk^intxf-comptftn/ Linux, W«ik Mac nein LJixrsiCht de* Compiler bzw. EnlwvcklüAftumgebuogen für €♦* Cnt wi C*]un<j,u«g»0un9»n f Ü«" <- 37
Zu den hier erwähnten Compilern muss ich natürlich noch anmerken, dass viele Eni- Wicklungsumgebungen die auf dem Betriebssystem vorhandenen Compiler verwenden und selten eigene Compiler miiliefem. (in Grunde sind ja die Entwicklungsumgebungen nichts anderes als eine grafische Steuerung für deinen Compiler und andere Werkzeuge. So verwenden bspw. unter Windows bekannte Entwicklungsumgebungen wicNetBeans, Eclipse, Qi Creator, Code::Blocks, Dev C++ oder das MinGW Developer Studio alle MinGW. Wobei MinGW wieder auch nur eine Portierung der GNU-Werkzeuge GCC ist. Bei einigen dieser Entwickhmgsumgebungen kannst du aber auch den verwendeten Compiler ändern. So kannst du bspw. bei Codc::Blocks auch auf den MSVC+*-Compiler von Microsoft zurückgreifen, wenn du bspw. MS Visual C*+ Express installiert hast. Ähnlich Ist es natürlich auch bei Linux-Systemen und Entwicklnngsumgebungen wie KDcvclop oder ?\nju(a, welche letztendlich auch wiederum nur auf GCC zurückgreifen. Xcode wiederum greift auf das Backend des LLVM-Compilers zurück. Wobei Clang das Frontend davon Ist. Somit sind die einzigen echten Compiler, die oben aufgelistet sind, der GCC (MinGW). Clang, der MSVC von VC++ und Intel C++ Compiler. Unter Linux-Systemen lassen sich einzelne Entwicklungs- umgebungen häufig ganz komfortabel mit der Paketverwaltung der entsprechenden Distribution nachinstallieren. Bist du auch ein Fan von Ubuntu, dann kannst du ja mal ein "sudo apt-get install unjuta g++-4.4w in der Kommandozeile absetzen und zusehen wie alles herunter- geladen und installiert wird. 38 EIKS
— ZWEI— in t,++ Elefanten können nlchtfllegen, aber Schrödinger kann programmieren Schrödinger schreibt sein erstes Programm und erfährt dabei gleich, was alles zu einem Grundgerüst gehört und wie solche Programme aufgebaut sind. Schrödinger Ist euphorisch, weil alles wie am Schnürchen läuft, und träumt schon von einer hoch- bezahlten Stelle als Entwickler bei seinem Llebllngs- splel WoW (World of Warcraft). Oder wie wäre es, wenn er nach Steve, Linus und Bill an DEM neuen Betriebs- system arbeiten würde?
Ein Computerprogramm liegt gewöhnlich als ausfahrbare Programmdatei. als soge- nannter Maschinencode, auf einem Datenträger vor. Startest du ein solches Programm, wird es zunächst in den Arbeitsspeicher des Rechners geladen. Anschließend über' nimmt der Prozessor deines Rechenknechtes die Kontrolle und verarbeitet der Reihe nach die für ihn lesbare Abfolge von Befehlen - das Programm bei der Ausführung eben. Nein, das geht auch nicht mehr mit einem normalen Editor. Bei einem Maschinencode handelt cs sich um für den normalbegabten Menschen nicht mehr lesbaren Binärcode, der sich nur noch mit ganz speziellen Maschinen sprachmonttoren lesen lässt. Keine Sorge, der Maschinencode wird natürlich wieder von einem speziellen Programm erstellt, welches mit einer ganz bestimmten Sprache gefattert werden muss, Diese Sprache ist für dich lesbar und als eine gewöhnliche Textdatei gespeichert, die du jederzeit wieder ändern kannst. Richtig! Du schreibst praktisch deine tur dich lesbaren C++ Befehle In einen Editor, und dann sorgt ein Compiler (und Linker) dafür, dass daraus ein Maschinencode bzw. das ausfahrbare Computerprogramm erzeugt wird. Wenn du dein Programm jetzt ändern willst, brauchst du nur den Quellcode mir deinen C++-Befehlen zu ändern und erneut zu übersetzen. 40 IMCI
Die Sache mit dem maln-Dlngs Irgendwo hat ja alles einen Anfang, also auch ein Programm. Die Mutter alle C++-Anfänge ist hierbei main(). Dabei handelt es sich um die Hauptfunktion (mam (engl.) = Haupt*), ohne die der Linker niemals ein ausführbares Programm erstellen könnte. Da die main()-Funktion also die erste Anlaufstelle deines C**-Programms ist, befinden sich darin auch die ersten Befehle des Programms, welche bei der Arbeit ausgeführt werden sollen. Hier die nackte tnain ( ) -Funktion, aus der sich tatsächlich auch ein sinnfreies ausführbares Programm erstellen ließe: Oa.i nt de» erste Einstiegspunkt eirws jeden €♦ ♦"Programm. wie viele andere FunklMwiefi d.iv«f Stehen oder (bwh noch folgen mOgen. Die einzelnen fiefatile der 1X13.111-Funktion werden zwischen cirx' sich öffnende und eine sich schließende geschweifte Klammer gesteift. Die werden als Anweisungsblock bezeichnet. int mainO { return 0; > Da die Funktion 103X11 ( ) einen Wert zurückgibt (will das int vor main( ) so haben) wird mit dem Befahl return der Wert 0 an den Aufrufer des Programms zurückgegeben. tritt Schritt« jn 41
(BHM* Genaugenommen musst du bei der Funktion main() nicht unbedingt (expli- zit) den Wert 0 zurückgeben, Tust du das nicht, macht main() das (implizit) für dich. Allerdings ist dies nur bei der main()-Funktion gültig und somit eine Ausnah meregelung. .....*/st’ j&r •<^Vt 4/* *Th 'c^x Schön, dass du mitdenkst! Die einzelnen Befehle werden n dem Semikolon-Zeichen am Ende abgeschlossen. Jeder Aus- druck, der mit diesem Zeichen endet, wird als Anweisung bzw. Befehl behandelt. Der Compiler weiß dann, hier ist das Ende des Befehls und arbeitet dann den nächsten Befehl ab. Immer langsam, die Welt wurde auch nicht an einem Tag erschaffen (theologisch betrachtet In sieben - oder waren es sechs??? aber wissenschaftlich Denk daran, dass man In C++ ganz streng zwischen Groß- und Kleinbuchstaben unterscheidet. So kannst du nicht einfach hergehen und Haln() statt main() schreiben. Der englische Fachbegriff hierzu lautet Case sensit ivity. 42 Uolt«l ZüCt
Unser erstes maln-Dlngs soll laufen... Jetzt Ist es an der Zelt, dass du deinen ersten eigenen Code eintippst und zur Aus- führung bringst. Es geht noch gar nicht darum, was welcher Befehl macht, sondern nur. dass ein Fenster aufpoppt und irgend was vor sich hinbrabbelt, Hier dein erstes Programm: *1 Dai ist ein Befett für den PräprozeiSOr Der Praproreisor iif ein weiteres Programm, welches noch vor dem Compiler ausgefuhrt wird Damit sagst du dem Compiler, dass dieser Befette verwenden kann. wekhe in de« Hcaderdatei bzw auch in der Bibliothek iostream enthalten sind. fincludc <iostrcam> "1 using namespace std; *2 *3 Dai sind deine eilten retten Befehle für das Programm Über cout und den Operator << wird der Text. der zwischen den Inc mainO { *2 Damit Mellst du für dein Programm den Namensraum std zur Vertilgung Das ist praktisch weil du ohne diese Zeile Befehle aus diesem Namensraum ent mit std:; cout oder std:; endl qualifizieren müsstest. Gänsefüßchen sieht, auf dem Bildschirm ausgegeben. Mittest du iostream am Anfang dei Listings m<ht angegeben, wurde sich das Programm nicht übersetzen lassen, weil der Compiler den Befehl dann nicht kennt cout « ''Elefanten können nicht fliegen, \n”; *3 cout « "aber Schrödinger kann programmieren^"; *3 return 0; tritt Schritt« in <•• 43
r<rn*rx*l — buh — JOm 11 Dieter Baer $ ./main Elefanten können nicht fliegen, aber Schrödinger kann progra»nioren Dieter Baer $ [J Dm wt mein eniei bei der Aw^fuhrunj juf meinem Mac. Otf' uiy^uburvtu:- Dat* tarbtfM Ans»cM SucMn Urrr«n>l »tu« userflubufitu: -5 ./prögramO«! Elefanten kennen nicht f Hegen, aber Schrödinger kann progrwiieren userßubuntui-5 | Htuteka. mein Programm Auf meinem UbvnlU'liiMiK Und tu gute* letzt dAt Auilübiende Progrannm unter MS Windows 44 C*O‘.t«l Jwtl
l*chiun«i Wenn falsche Umlaute in der AAS Windows-Boxangezeigt werden, hat das historische Gründe. In der klassischen DOS-Eingabeaufforderung wurde zur Kodierung des Textes die Codepagc 850 (Dos-Latin.1) verwendet. In der grafi- schen Oberfläche von Windows wird die Eingabeaufforderung hingegen mit Codepage 1252 (Win-Latin-1) kodiert. Natürlich giht es auch in C++ einige Mittel, um Umlaute deiner Kultur darzustellen, aber ich denke, der Zeit- punkt jetzt ist ungünstig, und dann würden hier nur noch mehr Fragezeichen abgcdruckl werden. Du könn- test dir bspw. selbst mit hexadezimalen Werten (bspw. eine UTF-8-Literale) behelfen, allerdings musst du hierfür dann wissen, welche Codepagc eingestellt ist. Auch findest du z. B. eine Klasse s td: : locale in der lleaderdatei <locale> für solche Zwecke vor. Du musst also .nur* ein neues Objekt von diesem Typ erstellen und das Objekt zu deinen Kultttrkreisen (.German*) anpassen. Folgendes Rezept könnte das Problem z. B. beheben: finclude <locale> // benötigter Header Hnclude <iostreatn> uslng nameapace std; int main() < locale loc; // Objekt erstellen II mit deiner Kultur initialisieren locale: :global (locale("German'*)); II Jetzt sollte es ein ö sein cout « "Schrödinger" « endl; tMORtCl Hier kannst du dich eines alten Tricks bedienen. Verwende einfach den MS-OOS-Befehl PAUSE dafür, welcher die Ausführung des Programms bis zum nächsten Tastendruck anhalt. Den Befehl kannst du in C++ mit der (leider) alten C-Funktion systemf) absetzen. Hierzu musst du zusätzlich noch die Headerdatei «esedlib* mit angeben, in der der Befehl enthalten ist. lincludc <iostream> fincludc <cstdlib> int maln(> < cout « "Elefanten können cout « "aber Schrödinger Einige Entwicklungsumgebungen kümmern sich selbst darum, bei anderen musst du dich darum kümmern. Im Grunde ist cs ja kein Fehler, wenn das Programm wieder verschwindet. Letzt- endlich wird das Programm ja von Anfang bis Ende ausgeführt. Leider findet die Ausgabe hier ja auf die MS-DOS-Eingabeauf- forderung stau, die extra für die Ausgabe geöffnet wird und sich daher am Ende auch automatisch wieder schließt, ohne dass einer gesehen hat. was drin stand. nicht fliegen,\n"; System("PAUSE"); Hier dein Rezept nochmals mit die- sem Notbehelf für schnellpoppende Windows-Fenster auf dem Zettel. Crstt Schritt« in <• 45
/ Anfall- Ia/oU/ linclude <loscrcatn> uslng namespace std; int Main() { cout « ,,WoW\n" } Rückgabe von main{) Endlich entspannen und träumen! Bevor du jetzt deine Bewerbung schreibst, kannst du mir sicherlich die Fehler rm folgenden Listing zeigen? Dich ftprt vermutlich ein wenig die lj.tch<* Haltung in Bezug auf return bei main( ). An dieser Stelle solllesi du noch wissen, dass es trotzdem immer int naainO heißen muss, Off Standard will, dass du hier immer ein Int vor mainf ) schreibst und nichts anderes sonst. Lass dir von keinem was anderes erzählen. Das schreibt der Standard eben so. und es gibt da keine Diskussion! (Kütil] In diesem Beispiel wurde der erste Buch- stabe von Main() großgeschrieben In C++ wird allerdings strengstens zwi- schen Groß- und Kleinschreibung unter- schieden. Außerdem wurde beim Kom- mando über cout das Semikolon <im Ende des Befehls vergessen Das Weg- lassen des Rückgabewertes 0 am Ende mit return ist in der main ()-Funktion kein Fehler und erfolgt (nur hier) auto- matisch. sofern nichts dransteht. 46 Ur.ttl Zwei
Kreuz und quer oder alles In Reih und Glied? Sinclude <iostream> using namespace atdjint main() Ein paar Worte möchte ich noch zur Formatierung des Qurllcodcs verlieren, ehe du eine wohlverdiente Pause einlegen kannst. Zunächst einmal kannst du deinen Code eintippen, wie du willst, im Grunde ist es egal, wie du das formatierst, solange ein Befehl mit einem Semikolon abgeschlossen wird und mehrere zu einer Funktion (oder auch einem Ausdruck) gehörende Befehle in einem Anweisungsblock zwischen den geschweiften Klammern zusammengefasst sind. Also folgendes Chaos ist ohne weiteres erlaubt: (cout « "Kreuz und quer\noder alles in Reih1' H und Glied...?!\nM;return 0;} Trotzdem lohnt es. sich die Mühe zu machen, den Code etwas ordentlicher zu formatieren. Ein gleichmäßiges Einrücken innerhalb eines Anwclsungsblocks hat noch niemandem geschadet. Auch ist es hilfreicher, wenn du nur einen Befehl pro Zeile schreibst. Das hilft dir im Falle eines Fehlers; und wenn es nur ein Typo in der Syntax ist, der so aufgrund der Fehlermeldung des Compilers schneller gefunden wird. Außerdem werden es dir deine Mit-Programmierer danken, wenn Sie deinen Code auch mal lesen oder überarbeiten müssen. Kein Kommentar? Du hast zwei Varianten, einen Kommentar In deinem Quellcode zu vermerken: "r Soll sich dein Kommentar nur über eine Zeile erstrecken, reicht die Zeichenfolge / /. Alles, was sich jetzt dahinter befindet, wird vom Compiler ignoriert. II Ich bin ein Kommentar Mehrzeilige Kommentare leitest du mit / * ein und been- dest diese mit * f. Alles, was sich also zwischen / * und * / befindet, wird vom Compiler ignoriert: /* Ich bin ein Kommentar */ (trlohfiungl So, jetzt kannst du dich zurücklehnen und von deiner künftigen Karriere träumen. trste Schritt« in <* 47
Um jetzt sinnvollere Programme zu erstellen, fehlt dir eine kleine Wegbeschreibung, wohin oder woher die Daten kommen, genauer, wie du etwas von der Tastatur einlesen kannst oder wie du die Daten dann auf den Bildschirm bekommst. (Htntrrgrundinfol Zunächst einmal Ist die Sprache C++ ohne die iostream- Bibliothek stumm und taub. Erst diese Bibliothek ermöglicht dir Wege, etwas von der Tastatur einzulesen oder etwas auf dem Bildschirm auszugeben. Das ist auch der Grund, warum du bei fast Jedem Programm die Headerdatei <iostream> mit angeben musst. Der Haupt- grund, die Ein- und Ausgabe von der Sprache zu trennen und als Bibliothek anzubieten, macht es wesentlich einfacher Ihr den Compilcrhrrstellcr, die Sprache auch auf anderen Systemen anzubieten. Daher kannst du deinen Quellcode auf deinem Mac-, Linux- oder aber auch Windows-Rechner übersetzen und auf die gleich Art und Weise verwenden. (Z'U'I) Das Konzept der iostream-Bibliothek ist natürlich weitaus umfangreicher und komplexer, als das jetzt hier beschrieben wird. Fürs Erste musst du dich hier erst einmal damit zufriedengeben, dass du diese Bibliothek für die einfache Ein- und Ausgabe verwendest. In C++ wird bei diesem Datenverkehr von einem St re am (engl. für [Daten-JStrom) gesprochen. Diesen Stream kannst du dir wie einen Tunnel vorstellen, durch den die einzelnen Bytes mit Daten einfach durchgeschoben werden, 48 ZvCl
Abhängig davon, aus welcher Richtung die Daten kommen, kann die Quelle eines Streams von der Tastatur oder einer Datei herrühren. In der anderen Richtung kann derZielort dann der Bildschirm oder auch wieder eine Datei sein. Du erzeugst quasi ein solches Strcam-Dings, welches fest mit deinem Programm verbunden ist, so dass der Datenverkehr des Programms dann nur noch über diesen Strcam erfolgt. Die grundlegenden Streams für die einfache Ein- und Ausgabe von Tastatur und Bildschirm von der lOStream Bibliothek lauten somit: Objekt Bedeutung Wohin/Waher cout SUndardiusgabc Bildschirm cerr S tandardf eh lera usgj.be Bildschirm clog SUndardfehicrAusg^bc (gtfHifftrt) Bildschirm ein Standardeingabe Tastatur Crit« Schritt« in <•• 49
*1 Kanivst du zur üblichen Ausgabe Aul dem Bildschirm verwundert. Unterwegs in Richtung Bildschirm gehl es also mit den Sircam-Dingern cout, cerr und clog. Zum Beispiel: cout « "Auf zum Bildschirmen"; ’1 cerr « "Das auch...\n"$ *2 •z ist die clog « "Und noch SundardAusgibe nicht mehl vcrfügbai oder tritl ein Fehler aut wlhest du diesen Stream verwenden Im Gegensatz zu cout wird keine Pufferung für diese Ausgabe verwendet einer**3n"; *3 •3 Diesen Stream kannst du verwenden, wenn du Kem trollmeld ungen w< dem Bildschirm ausgeben willst. Im Gegensatz zum Stream CCtt wird die Ausgabe allerdings gepuffert. Sinnvolterweise wird der Datenverkehr mithilfe von Puffern geregelt, Stell dir mal vor, es wird jedes einzelne Zeichen sofort über den Stream auf dem Bildschirm ausgegeben oder in eine Datei geschrieben. Wenn jedes Programm auf dem Rechner so vorgehen würde, gäbe es wohl keine Interaktion mehr mit dem Rechner, und der Prozessor wäre bis zum Anschlag beschäftigt. Zudem sind diese Arbeiten außerdem nicht unbedingt die schnellsten Aktionen und würden den Rechner unnötig aus- bremsen. Daher gibt es einen Puffer, der wie eine Badewanne gefüllt werden kann, bis nichts mehr reinpasst. Erst danach kann man den Stöpsel ziehen, und das Wasser kann wieder ablaufen. Natürlich gibt es hier auch Ausnahmen, und man kann den Stöpsel auch schon bei einer halbvollen Badewanne ziehen* 50 Ciphtl ZMCl
(HäntrrgrundinM Der Stream cerr arbeitet ungepuffert, weshalb die Ausgabe sofort auf dem Bildschirm durchgeführt wird. Bezogen auf die Badewanne entspricht dieses Verhalten dem Einlassen von Was- ser in die Wanne, obwohl der Stöpsel nicht abgedichtet wurde. Das Wasser läuft also vom Hahn sofort m den Ausfluss. In die andere Richtung geht es mit dem Datenstrom ein. welcher in der Regel für das Hinlesen von der Tastatur ver- wendet wird. •1 Dm ist int («Integer - ganze Z.ihl) niil dem N.imen gibmirfuenf Int gibmirfuenf; *1 cout « "Gib ihn fünf: " ein » glbmlrfuenf; *2 *2 Hier kannst du gibmirfuenf über die Tastatur geben, was e< haben will Damit die Ströme fiirdie Ausgaben cout, cerr und clog bzw. der Eingabe ein richtig funktionieren, musst du den Ausgabeoperator « und den Eingabeoperator» verwenden. Heide Operatoren sind so implementiert (überladen, um genau zu sein}, dass du damit die grundlegenden Basisdaten typen ohne besondere Vorkehrungen ausgeben und einlesen kannst. Natürlich sollte dir dabei klar sein, dass du den Ausgabcoperalor nur für Ausgabe Streams und den Eingabeoperator nur für den Eingabc-Stream verwenden kannst. trsts Schritts tn 61
Gegenseitige Wahrnehmung. Jetzt ist es an der Zeit, die einseitige Beziehung zwischen dir und deinem Pro- gramm zu beenden, Das folgende Beispiel zeigt dir eine gut gepflegte Beziehung zwischen dem Bildschirm und der Tastatur: Hnclude <iostream> using namespace std; int niain() *3 Da def Operator« überladen werden kann, lasst sich dahinter immer noch weiterer Text mit einem weiteren << naefwehwben. D.n cndl am Ende ist ein kleines Hctfcrfeln (mit dem mächtigen Namen Manipulator} und hiipli für uns in die nächste Zeile. ine giboiirfuenfi cout « "Gib mir fünf: ein » gibmirfuenf; *2 cout « "Danke für die return 0; } •1 Die Aufforderung für eine Eingabe. '2 Hier musst du w eine Ganzzahl eingeben ’ über den Operator » wird die Ganzzahl nach gibmirfuenf geschoben, ” « gibmirfuenf « endl •3 FehlecJ Der Datenverkehr über das ein-Dings von der Tastatur wird an der Stelle beendet, an der das erste Zeichen nicht mehr bearbeitet werden kann. Wenn du bei der Eingabe von gib- tnirfuenf den Wert „5Meter* eingegeben hast, so wird in gibtnirfuenf nur der Wert .5* gespeichert. Das liegt daran, dass du als Typen einen Integer verwendest. Führende Leer- räume hingegen werden völlig ignoriert. 52 UeitH zwei
Manipulieren oder selber steuern? Um bei der Ausgabe in die nächste Zeile zu springen, hast du mehrere AAOglkchkeiien: 1. Entweder du manipulierst mit endl, flüchtest mit * \n*. 2 Oder du flüchtest und terminierst mit11 \n’r. iNotir) Alle drei Zellenende-Ringer kannst du über den Ausgabeoperator « in einen Ausgabe-Strcam stecken. 4 Als vierte Möglichkeit kannst du noch den Ausgabeoperator etwas schonen und das Zeichen für das Zcilcncnde am Ende des Textes hinzuiugen (zum Beispiel: "Schonendes Zeilenende\n"). /eZC «v~,ss c&e/i Bei den Zeichenkombinationen, die mit dem Zeichen \ beginnen, handelt es sich um nicht darstellbare Steuerzeichen, die auch Escape-Sequenzen (escape entfliehen, entkommen} genannt werden. *1 Der Manipulator endl stellt d>c langsamste Version da. weil noch erne Extra-Synchronisation diMgefDhft wird. Und zwar werden hier alle sich noch itn Puffer befindlichen Daten wfcMt an die Ausgabe .geschickt. *3 Hier wird neben dem Zcikncndwexhcn noch ein zusätzliches Uber nicht sichtbares! Terminierungs- Zeichen verwendet, welches das Code des Swings kenn- zeichnet (Hdsta La visu, babyf). finclude <iostream> using namespace std; int main() < ein eiiweines Zeichen verwendet wird. cout « "Der Manipulator” « endl; *1 return 0; 1 cout « "Das Zeichenliteral”’ « •In1; *2 cout « "Das Stringliteral" « rt\n*; ’3 cout « "Oder gleich im Stringliteral\n"; *4 ’4 D»e wohl für den Pro/eswr günstigste Losung womit du den Adv gnbraprr.itor << nicht npchm.ik extra belasten musst Crit« Schritt« <• 53
Noch ein wenig Brain-Streaming Eigentlich sollte Jetzt Zeit flkr eine Runde WoW sein, aber vorher wollen wir den Tag noch ein wenig überIIlegen. Du weißt jetzt, dass der Datenverkehr in C++ über Streams realisiert wird. Zwar hat das Stream-Konzept noch viel mehr zu bieten, aber für die nächsten ßuchscitcn bist du hiermit erst mal gut gerüstet. Du weißt Jetzt auch, dass diese Streams nicht Bestandteil der Sprache, sondern als Extra-Bibliothek enthalten sind. Des Weiteren hast du auch die Operatoren « und » kennengelernt, die dir dabei helfen, die Daten korrekt In den Siream zu stecken. [tinUcKf Aufgibel Bevor du dich also auf neue Quests in der Welt der Kriegskunst stürzen kannst, möchte ich noch sehen, ob du das Thema verstanden hast. Du solltest jetzt in der Lage sein, im folgenden Listing ohne Lupe und Rechner, die Fehler mit dem bloßen Auge zu erkennen. finclude <Lostreaa> int taainf) < int Level; cout » •»Welchen Level hast du in WoW: ein « Level; cout » "Wow, Level •* » Level » "! Nicht schlechtln"; return 0; 54 c«0if I Zwei
Richtig, aber damit dieses Beispiel auch läuft, fehlt noch etwas ganz Wichtiges. Okay, ich helfe dir. Wenn du Jetzt die Operatoren auf den richtigen Weg bringst, gibt es trotzdem keine Verbindung zu den Streams cout oder ein, weil diese nun mal keiner kennt. Wie ein Vampir, der nicht ins Haus kommt, wenn du ihn nicht extra herein- gcbelen hast, musst du auch die beiden Streams in den Gültig- keitsbereich bitten. Es fehlt praktisch der Namensbereich Std in diesem Beispiel, den du bisher immer mit using natnespace std; hereingebeten hast. Alternativ kannst du aber auch den Namensbereich Std mit dem Bercichsoperaior : : reinbitten: std:: cout « 11 Du darfst rein" std::endl; So, jetzt kannst du einen neuen Quest anfangen Crst« Schritt« 55

Die C++ Basistypen Verschiedene Typen für einen bestimmten Zweck „Wohin mit den Daten?“, fragt eich Schrödinger und bemerkt, dass es dafür verschiedene Basisdatentypen gibt und dass C++ au ordern sehr pingelig ist, was diese Typen betrifft. Hierbei erfährt er auch gleich, wie die einzelnen Zeichen und Buchstaben in seinen Computer gelangt sind und wie er sie da wieder heraus und auf den Bildschirm bekommt.
Starke Typen Um Daten speichern zu können, benötigst du eine Kiste. Statt einer geöffneten Kiste findest du hier allerdings eher Öffnungen mit geometrischen Formen, wie du das noch von Steckspielen aus deiner Kinderzcit her kennen könntest. Das bedeutet also, dass die jeweiligen Formen nur In bestimmte Öffnungen pas- sen. So ist das auch mit dem Speichern von Daten. M*n kann nicht alles teinttockrn, wo und allen* was man w»-l ... Ähnlich, wie du bei WoW als Priester heilen kannst und als Krieger eher nicht, legst du mit einem Typ in C+4 auch gleich fest, was du damit alles anstellen kannst. Schau dir einmal folgende Zeile an: ehe = mann + frau; der fundamentalen Typen stecken, aber später lernst du noch, wie du mit den fundamentalen Typen eigene benutzerdefinierte Typen erstellen kannst. Du könntest quasi aus den grundlegenden Basistypen einen eigenen Typ Kuh erstellen. Damit diese Zeile in C*+ einen Sinn ergibt, müssen mann und f rau vom passenden Typ sein, damit die Addition (+) und Zuweisung (®> verwendet werden kann und (eine) ehe dabei hcraiiskommt. 56 Mtci
Mein Name ist Schrödinger Djtmil jede« wr-ntn Stein wtcderbtkörnrm, müisen eindeutige Namen verwendet werden. Um Jetzt auf die Daten zugreifen zu kön- nen, benötigst du flir jeden Typ einen Namen (genauer einen Bezeichner). Für einen solchen Namen kannst du beliebige Buchstaben, Ziffern und das Unterstrich- zeicheii (z. B. _huhu) verwenden. Das erste Zeichen darf allerdings keine Ziffer (somit falsch: 8tung: sein, und natür- lich musst du auch hier auf die Groß- und Kleinschreibung achten, Somit sind Nebelleben und nebellebeN zwei verschiedene Namen (egal, In welcher Richtung diese gelesen werden). Natürlich stellt dir C++ hier grundlegenden Typen zur Verfügung, in denen du deine Daten speichern kannst. Grund- legend heifit hierbei: Ganzzahltypen, Gleitkommatypen und Typen für Zeichen. Wenn du diese Typen besser kennengelernt hast, kannst du spater daraus speziellere Typen basteln, Stell dir das einfach wie bei dein chinesischen Legespiel Tangram vor, bei dem du aus primitiven geometrischen Flächen unzählige Figuren legen kannst. Ebenso kannst du es in C++ mit primitiven Typen recht weit bringen und daraus besondere Typen Zusammenlegen (bspw, auch eine Kuh). >1» (»«‘hsBtyptr 09
Wie überall gibt es auch hier wieder Typen, die gar nichts tun wollen und sich jeder Information entziehen. Genauer handelt es sich hierbei um void. Der Typ kann sich gar nichts merken, und man kann ihn höchstens bei Funktionen gebrauchen. Der sinnleere Typ könnte zwar auch als Zeiger verwendet wer- den, aber das gilt als böse und sollte in C++ mit Weihwasser gebannt werden, » Gleich noch eine Weisheit, mein Guter Bevor du einen Namen in deinem C++Programm verwenden kannst, musst du dem Compiler Bescheid geben, mit welchem Typ du daherkommst. Ein gegenseitiges Bekanntmachen gehört schließlich zum guten Ton. und ohne kommst du ohnehin nicht rein. Eine solche Verknüpfung zwischen Typ und Namen wird als Deklaration bezeichnet. Damit weiß der Compiler, wel- che Aktionen du auf das Speicherobjekt anwenden kannst (bspw. +. / usw.). Eine einfache Deklaration kannst du dir so vorsteHen: *1 Grfn'ßt van Typ. g.bst du den Namen für das SpeKhcrobjekt an. Am fnde schließt du diese Deklaration mit einem Semikolon ab. Typ Name; *1 Typ NameOl, Name02, Nacae03; ‘2 |2«ttrl| Das Thema Deklaration und Definition ist nicht auf einfache Typen beschrankt. Auch konstruierte Typen und Funktionen müssen deklariert wer- den. Allerdings kommt es dabei häufig vor, dass du die Definition an einer anderen Stelle im Quellcode vornimmst als die Deklaration. Darüber brauchst du dir aber jetzt noch keine Gedanken zu machen. •2 Getrennt durch ein eiflbches Komma lassen sich auch gleich mehrere Namen vom selben Typ deklarieren. Meistens sind solche Deklarationen von Datentypen gleichzeitig auch Definitio- nen. Das heißt, es wird ein eindeutiges Objekt Im Speicher erzeugt. dem alle nöligen Informationen (eben was der Typ alles kann} zugeordnet werden. «0 C«»St+l D*CI
Offiziell bietet Co derzeit verschiedene Grüßen von Ganz- zahltypen (=lntcger) an. Da wären (in aufs leigender Größe) short int. int, long int und long long int. Wie du an dem Namen short int schon herauslesen kannst, handelt es sich um den kleinen Bruder (Geschlecht der Typen Ist leider nicht feststellbar) und bei long int um die große Schwester (im Sinne der Gleichberechtigung) von int. Die beiden, short und long, können auch ohne den Familiennamen int verwendet werden. » Ein Blick in die Glaskugel IHintf r^rundinktj long long int ist ein extra laaanger Ganzzahltyp. der offiziell erst mit dem C++11-Standard dabei ist. Dies wollte |Z«1tefl Der Erfinder von C++. Bjarne Stroustrup will hierbei gerne einevereinheitlichte Initialisierung sehen. Mit dem C++11- Standard wurde eine solche vereinheit- lichte Initialisierung mit geschweiften Klammern eingeführt, welche sich neben einfachen Datentypen, die eben beschrieben werden, auch spater für komplexere Typen wie Arrays, Struk- turen. Klassen oder verschiedenen Behälter-Klassen (Container-Klassen) verwendet werden kann. Anstatt einer klassischen Zuweisung mit wirst du hier in naher Zukunft auch häufiger die Version mit den geschweiften Klammern antreffen: int dictcr{36); // Vereinheitlichte II Initialisierung (C++11) int helmut{dieter); II Das geht auch mit C++11 ‘4 helmut bekamden- sdben We*t wie dictcr angewiesen ich nur erwähnen, falls du einen alten Compiler hast, der damit nichts anfangen kann. Wenn du eine lokale Variable mit Namen anlegst, bedient sich dieser erst einmal selbst und greift einfach nach einem Werl, der da eben zufällig rumliegt. Dass ein solches Verhallen meistens Probleme mit sich bringt, sollte klar sein. Daher solltest du dich immer darum kümmern, dass eine Variable gültige Werte erhält. Hierbei gibt es mehrere Wege, von denen der grund- legende wohl die Zuweisung mit dem s ist. Bei neueren C++11- Compilern kannst du auch die vereinheitlichte Initialisierung mit den geschweiften Klammern zwischen (} verwenden. Natürlich kannst du die Werte auch über den Stream ein und den Ein- gabeoperator in die Variable schieben: 1 dieter wird der Wert 36 zugewiesen int dieter - 36; *1 short kleinerDieter, nena long grosscrDictcr; *3 ein » groaserDiecer; *3 int helmut • dieter; -4 long schroedInger; *5 schrocdingcr 1234; *5 *2 kleinDieter m nicht initialisiert und nena bekommt den Wert 99 zugewiesen = 99.-2 ‘3 grosser- Dlet C r bekommt seinen Wert über den Eingabe-Stream (der Taiutuf) ein mit dem Operator » zugeschoben *5 Variablen müssen nicht sofort nitialisiert werden. 91« C---8asistyp«n ei
Es gib« gewisse Regeln, die du bei den Zeichenfolgen einhallen musst, damit der Com- piler die Erscheinung auch als Ganzzahl akzeptiert. Du wirst zwar anfangs meistens nur die dezimale Schreibweise (Basis 10> verwenden, aber daneben kannst du auch eine oktale (mit der Basis 8) und hexadezimale (mit der Basis 16) Schreibweise benutzen (das Zeichenlitcral lassen wir mal außen vor). dezimal 0 1 100 25$ oktal 00 01 0144 0377 hexadezimal 0x0 0x1 0x64 Oxff Ein Beispiel mit oktalen und hexadezimalen Gegenstücken tz«u«n Bei der hexadezimalen Schreibweise werden die Buchstaben a. b, c, d. e, f verwendet, um 10, 11. 12. 13. 14. 15 darzustellen. Alle drei Ganzzahltypcn haben eine positive und negative Hal tung. was Ihren Werteten betrifft. Wenn du einen Integer ohne weitere Angaben hinschreibst, kann dieser sowohl negative als auch positive Werte aufnehrnen. Brauchst du allerdings einen ganzzahligen Wen, der nur vorzeichenlos sein soll, stellst du lediglich das Schlüsselwort unsigned voran, und schon hat der Typ seine negative Haltung fallen gelassen: *1 Die Variable diese» Typ» kann dci Srhruwlwort» unsigned keine negativen Wert- speichern. "3 Hie* kennzeichnest du mithilfe des Schlüsselwortes Signcd die Variable ncutralerTypO2 explizit als vofaeichenbehaftet. Au* das Schlüsselwort kannst du verzichten, weil gjnizj-hliflc Typen ohne die Verwendung von unsigned immer van-eichen- behaftet sind, 62 Upitti mci unsigned ine posiclverTyp? r1 int neutralerTypGl; -2 signed int ncutraltcrTyp02; *3 *2 Oie Variable dieses Typ! speichert negative und positive Werte.
Von Us und Ls JAbUffJ Hier noch ein« Schnellnotiz, wie du deine Zeichenfolge von Ganzzahlen noch kompli- zierter machen kannst. Hängst du am Ende ein U an die Zahl, gilt diese Zahl explizit als unsigncd (r.B 1234U). Hängst du ans Ende ein L, wird die Zahl explizit als long int betrachtet (z.B 1234U Kombinierst du beide miteinander, kannst du auch ein Wenn du heraus finden willst, ob deine Freundin dir treu ist, kannst du den Booleschen Typ verwenden. Der iyp bool ist nur darauf Spezialisten, ob etwas wahr (=true) oder unwahr (=f alse) ist. Damit kannst du die verschiedensten Werte bzw. Ausdrücke auf Wahrheit hin testen. Ein einfaches Beispiel: *1 Wenn dieter und eva den gleithen Wert hüten, wirc der Wert wn treu gleich true Im vorliegenden Fall haben d.-rse beiden Variable untefxhiedhclie Werte. Daher hat treu hier den Wert false Sieht wohl nicht gut aus für die beiden... ‘2 Hier Hl ich* LiebeDich auf jeden Fall true, weil eine ganze Zahl ungleich 0 immer mrh true konvertiert wird Hierbei wird der wert 1234 wh i btruei konvertiert Int dieter-1974, cva-1980; *1 bool treu dieter««eva; *1 bool IchLiebeDich « 1234; *2 Int liebe ichtiebeDlcb; -3 int wahreLiebe • 0; bool ILoveYouBaby wahreLiebe; •3 liebe bekommt hie* den Wert 1 zugewiesen, weil ein Boolescher Wert eher nur die Werte true (1) oder f alse (0> enthalten kann *4 M iLöve- YouBaby hingegen ist false, weil ein Wert von 0 wa* wahreLiebe eben Ijt, immer wh false konvertiert wird. di« *hjistyp*n 63
Ia/^S w bool Werter Schrödinger, zu argwöhnen, das Seiende sei anzweifel- bar, ist der erste Schritt, zwischen wahr und unwahr zu unterscheiden. Um deine Anfrage als Wahres zu beantworten, Folgendes: Du wirst die Logik erst erkennen, wenn du weiter an deinem Sein arbeitest und später in Logik- anweisungen oder Funktionen zwischen Wahrem und Unwahrem unterscheiden willst. «4
Was nehmen wir für einen Typen? Nachdem du die Typen für Ganzzahlen kennst, wird es Zeit, dass du diese auch mal benutzt. Du solltest dir dabei jetzt noch keine Gedanken bezüglich der Größe der Typen machen. Das klaren wird später noch. (Cinf^ch* Aufjsbe) Erstelle ein einfaches Programm, welches deinen Kontostand abfragt und auch, wie viel Gramm Wurst du vom Metzger Meier gerne hättest. Überlege dir, was du bei den beiden Variablen kontostand und gramm beachten solltest. Hierzu eine mögliche Lösung #inclu.de <iostrcam> using nftmespace scd; int main() { M Hier könntest du genauso gut den Typ short oder Long verwenden (hängt natürlich von der persönlichen Finanzlage ab). Wichtig ist auf jeden Fall. dass hier ein Typ mit Vorzeichen verwendet wird, weil nun das Konto ja auch ütXTZieben soll, damit die Bank auch was dtan verdient •3 Über den Eingdbe-Stream ein schiebst du mithiHe drr »‘Operatoren die Wc*te von der Tasuiur m die entsprechenden Variable. int kontostand; ‘1 unsigned int gram»; 2 -2 Hier brauchen wü mit unsigned einen vorzeichenlosen Typ weil ca eben keine 100^“ Wurst gibt. cout « "Dein Kontostand ein » kontostand; *3 cout « "Wieviel Wurst darf es ein: ein » gramtn; *3 cout « "Kontostand : " « kontostand « "\nWurstaufschnitt : " « grairan « " gra®aH « endl; return 0; lio C•--Bisi05
Die Welt der ganzen Kerle Neben den ganzzahligen Kerlen hast du Jetzt auch erfahren, wie du eigene Variablen anlegen und verwenden kannst Das gilt übrigens auch für die kommenden Typen, die dir noch begegnen werden. Hier noch eine kleine Checkliste, was du dir ft)r die Ver- wendung von Typen hinter die Ohren schreiben solltest: Dann war da noch der Aristoteles-Typ mit der Wahrheit, bool hieß der und kann nur die Werte 1 und 0 darstellen. Also alles, was ungleich 0 ist, betrach- tet bool als 1 oder eben als Synonym true, und alles 0-wertige ist eben 0 oder das Synonym false. Ich weiß zwar immer noch nicht genau, wozu dieser Typ eigentlich gut ist, aber das wird schon noch kommen. Kann ich jetzt endlich schlafen gehen Du brauchst einen eindeutigen, gültigen Namen (Bezeichner), der aus beliebigen Buchstaben. Ziffern und dem Unterstrich- Zeichen bestehen darf. Dabei wird auch zwischen Groß- und Kleinschreibung unterschieden! Wie bei l.cgobaustcincn gibt es in C++ zunächst nur fundamen- tale Typen (Basistypen.). Aus diesen Typen kannst du später komplexere erstellen {ja. auch Klassen). Jeder fundamentale Typ (neben den Integern gibt es noch ande- re) hat ein seiendes Ding von etwas (genauer Entität), eine Lis te von Fähigkeiten eben, was mit dem Typ möglich ist und was nicht. Bevor du diesen Namen verwendest, musst du dem Compiler Bescheid geben, mit welchem Typ du gehen willst (Deklara- tion). Auch zu den ganzen Typen, die dir in diesem Kapitel vorgestelh wurden, kannst du noch Folgendes in dein Gehirn einfügen (einfaches Copy & Paste eben): Ganzzahhypen gibt es mit short (int), int und long (int) und long long (int) in drei verschiedene Größen. Wohnt der Typ im selben Haus (lokaler Typ), hat er ohne eine direkte Inhiallsienmg zunächst keinen gültigen Wert. Standardmäßig haben alle Ganzzahltypen ein Vorzeichen (Signed). Soll das Vorzeichen verschwinden, musst du das Schlüsselwort uns igned davorsetzen. l*doh*iun(/lo»u'igl Jawoll! 66 ucitii
In C++ wird <j* U5-SCHRE I8WEI5E lü« die CleitkommAtypen verwendet. Und da wird d» Pünktchen dem Komma vocgerogen. Folglich könnte mxn zwar auch von GLEIT- PUNKTTYPEN reden, w» allerdirig» dann wieder nächt unterer Kultur pa$it. weil rk das t>ei uns halt nicht gibt. Daher wurde FLOATING POINT einfach an d*e lokalen Gegebenheiten angepasst und als GLEIT* KOMAAATYP übersetzt. Denk also auch dar an, dass du bei der Eingabe tänen PUNKT statt eines Kommas verwendest. Ursd, ja, theoretisch ist ei möglich, den Punkt zum Komma u mzuoperiefen, aber In der Praats ist das eher unüblich. Auch bei Zahlen mit Kommas wirst du mit verschiedenen Größen versorgt. Mit float für einfache Genauigkeit, double für doppelte Genauigkeit und long double für erweiterte Genauig keil stehen dir drei fundamentale Typen zur Verfügung. Die exakte Genauigkeit ist übrigens abhängig davon, wie die Compilcrbauer diese implementiert haben. Lieblingstyp und vom Compiler bevorzugter Typ ist immer double. Auch bei den Kommatypen gibt es einige Regeln, damit die Zeichen am Ende auch als Kommatyp angenommen werden. Hier einige Beispiele: 123.456 .33 0.33 2. 2.0 2.2el0 2.22e-14 Steht vor oder nach dem Pünktchen eine 0, kannst du diese auch weglassen (bspw. macht der Compiler aus .5 gleich 0.5 oder aus 3. ein 3.0). Ein Leerzeichen darf allerdings bei der Zeichenfolge eines Gleitkommatyps nicht stehen. Von fs und Ls lAbli««] Wie auch bei den Ganzzahlen kannst du auch hier ans Ende der Gleitkomma-Zeichen- folge noch einen Buchstaben setzen, um aus dem Liebling double etwas anderes zu machen. Hängst du den Buchstaben f ans Ende (bspw. 3.1415f. 3.7e-3f) stellt die Zeichenfolge einen float dar. Mit einem L am Ende kannst du ein long double anzeigen (bspw. 3.145L, 3-7e-3L}. Das Thema Initialisieren und Bekanntmachen wurde bereits bei den ganzen Typen behandelt und gilt hier natürlich genauso Einige Beispiele: float mynamcislittledot .535; // 0.535 double daddysdarling • 3.1415; long double extendenddot = 1.9e-3f; double CpplIncwstandardf123.456}; // mit C++11 möglich 01» C «‘Qjsi 67
Das Pünktchen In der Werkstatt Zu dem Thema willst du stchcrll<h auch ein Beispiel schreiben, wie du einen Gleitkommatyp von der Tastatur ei niesen kannst: Gib ihm eine Zahl mit Pünktchen ... linclude <iostrcam> using namespace sed; int main() < double willPuenktchcn; *1 ‘1 Bei diesem sir»n1rcien Beispiel kanost du genausogut den Typ float <xteu long double statt double verwenden. Ich habe mich halt gleich für Daddys Liebling entschieden cout « "Gib ihm eine Zahl mit dem Pünktchent "; ein » willPuenktchen; ’2 cout « willPucnktchcn « endl; return 0» > *2 Es wird alles cfngelescn. solange die Regeln der gültigen Zeichen- folge (Uteral) eines GleiUcommatyps eingehalten werden. Gibst du bspw 3#55 ein, bricht ein ib dem Komma ab, und willPuenktchen enthllt 3.0. Nicht vefgcswn: Pünktchen statt Kommas! $c//Z * Tut mir leid, aber hier kann ich dir vorerst keine feste Empfeh- lung geben. Das Problem ist leider, dass es von der Imple- mentierung abhangt, wie die Kommatypen dran und drin sind Daher ist der Sinn von einfacher, doppelter und erweiterter Genauigkeit auch abhängig von der Einpflanzung der Typen im Compiler. Hier bleibt dir zunächst nichts anderes übrig, als dich mehr mit dem theoretischen Thema Gleitkommazahlen aus- cinanderzuseizcn (Wikipedia ist dein Freund). Wenn du dazu keine Lust hast, kannst du ja immer noch Daddys Liebling double verwenden und schauen, was passiert. 68 c«,kt«i MCI
float, Am Ende war das Pünktchen double Ach, Schrödinger, schonmal was von Gleit- komaazahlen gehört? düubL ist ja gut und schon, aber über Gleit- kommazahlen musst du dich schlau machen. Ich erklär’s dir gerne ™ An die Arbeit geht’s. Theorie kann warten. Findest du die Fehler in dem folgenden Codeausschnitt? float floaty_l 3. 3334f; float floaty_2 ,3335f; double doubly = 3.3elO + 10; long double Idoubly 2.34e - 11; Bin gespannt, ob du es lösen konntest. Hier die Lösung *1 Hier steht ein Leerzeichen nadi dem Punkt, was f.ikch Ixt. •2 In dieser Zeile wird ein Komma statt einet Punktex verwendet float floatyj - 3>3334f; e1 float floaty_2 - .3335f; *2 double doubly 3»3el0 * 10; “3 long double Idoubly 2..34e-ll; 3 Hier iit rein syntaktisch kein Fehler vorhanden Che Addition mil 10 ist kein Fehler. Aber ob d.« hie* w gewollt war. blcibl unklar?! ff l*elotaun(SLP»u*ig! Jetzt ist aber eine dicke Belohnung drin: die neue Blu-ray .Angriff der achtbeinigen Monster-* •4 H«er ist ex aber ein Fehler, weil der Exponent le) noch Ziffern benötigt, damit es sich um eine gütige Zeichenfolge einer Gleitkommazahl handelt Beide Leerzeichen -or und noch dem iTiinusSymb-al sind in «fern Fall also falsch. Di« -B«si»typen 69
Wenn du einzelne Zeichen darstellen willst, musst du den Binärcode (Darstellungen 0 und 1) ver- wenden. Nein, so weit musst du glücklicherweise nicht gehen. Für Zeichentypen stellt dir C++ den fundamentalen lyp char cur Verfügung. Meis- tens (muss aber nicht sein!) ist char mit 8 Bits ausgestattet. In diesen 8 Bits lassen sich somit theoretisch 256 Zeichen (2* 256 verschiedene Bit-Darstellungen von 0 und 1) speichern. Es gibt ausgefallene Rechner, wo ein char nicht auf acht Beinen daher kommt, aber ich hoffe, dass dir solche »Monster* nicht über den Weg laufen. Um hier nicht wie in einer An Zweistromland mit einer Kolonne von 1 und 0 einen Buchstaben darslellcn zu müssen, existiert ein Zeichensatz dafür. Du kannst dir diesen wie eine Tabelle vorstellen, in der char sein Zeichen für einen bestimmten Code erhält und benutzen kann. Meistens (muss auch nicht sein!) wird hier der ASCII-Code verwendet. Der ZrülWnlyp Ch^t* kion ein vZeicM«i" im» einem Zeichematr speichern (tspw. ASCri) (Hinterfrundinfa) In der Praxis kannst du dich meistens darauf verlassen, dass die ersten 127 Zeichen (0 bis 127) im (US-)ASCII-Code die Dezimalziffern, die 26 Buchstaben des englischen Alphabetes und andere grundlegende (auch nicht darstellbare) Zeichen enthalten. Schwieriger sieht es dagegen meistens beim Kodieren von landestypischen (diakritischen) Zeichen aus, wie du es von deinem Rechner mit den Umlauten her kennst. Hierbei musst du dich dann leider mit dem verwende- ten Zeichensatz auf deinem System auseinandersetzen. Als universelle Lösung könntest du UTF-8-Literale wie uS'XuOOfc" oder die Locale-Komponente von C++ verwenden. 70 Ut.lUI Ott!
Damit, der Compiler ein Zeichen auch als solches annimmt. musst du es zwischen einzelne Anführungszeichen setzen (z. B. 1 A' oder 1 Z1). Genau genommen ist allerdings eine solche Zeichenfolge (genauer Zeichenliterale) allerdings nur ein symbolischer fester Wert ftir den ganzzahligen Wen des Zeichens im Zeichensatz. Verwendet dein Rechner (höchstwahrscheinlich) den ASCH-Zeichensalz, so ist der Wert ’ A1 gleich dem Wert 65. Somit hätte folgende Verwendung von Chat dieselbe Bedeutung: char Symbol fucr A • ’A’j ’y > / / char Dezlmal_fuer_A = 65; ** ZyUf C>b Nur, wenn du dir sicher sein kannst, dass auf jedem Rechner derselbe Zeichensatz läuft, schon, was aber laut Murphys Gesetz nicht immer der Fall ist. Wenn auf einem System ein anderer Zeichensatz verwendet würde, wäre die Hochkomma Variante portabler. Denn dann müsstest du dich nicht um den tatsächli- chen Wert des Zeichens kümmern, da dir das ja in dem Fall der Zeichensatz abnähme. Hurra, die Werte zwischen den Bereichen 0 bis 127 schaffen in der Regel kaum Proble- me. Alle anderen Werte außerhalb des Bereiches leider schon. Sätze wie .Schrödinger könnte grüne Grütze rühren" könnten somit auf unterschiedlichen Rechner zu inter- essantem Zeichcnsalat führen. Mit UTF-8 und Konvertierungen kann man das Problem in C+411 ganz Allgemein und portabel schaffen. Es kommt noch schlimmer (Achtung] Jr" Bei char hängt es von den Compiler- bauern ab, ob char als signed (-128 bis 127) oder als unsigned (0 bis 255) mit den Zeichen implementiert ist. Das ist natürlich ein weiterer Grund, bei char auf Dezimal- zahlen zu verzichten 71
Nachdem cs C++ also nicht kümmert, welcher Zekhensatz verwendet wird, musst du dich selbst mit dem Thema herum- schlagen. Aus purem Egoismus (oder Patriotismus) der US- amerikanischen Ingenieure wurde der ASCII-Zekhensatz auf den ersten sieben Bits verteilt und das achte Bit als Paritäts-Bit ver- wendet. Wir Europäer hatten somit keinen Platz mehr für die landestypisehen Zeichen wir üäößÜÖA oder die anderen euro- päischen landestypischen Zeichen. Eine Gruppe von Leuten (die ISO) haben sich darum gekümmert. Sie haben den ASCH Zeichensatz auf 8 Bits aufgebohn und diesen Zeichensatz unter Bezeichnungen wie 1SO-8859-1. ISO 8859 2 usw. zusaininengelässt. Unsere Umlaute findest du daher in lSO-Latin-1 wieder. Allerdings ist es nicht damit getan, den Zeichensatz der Eingabeaufforderung einfach auf ISO-Uiin-1 umzustellen, weil es sonst wieder Probleme mit der viel weiter verbreiteten UTF-8-Kodierung für Unicode gibt, die auf modernen Compu- tern zur Darstellung von Umlauten verwendet wird. Hinzu kömmt noeh die Eingabeaufforderung von Windows, welche aus Kompaübiliiätsgründen immer noch am alten IBM-PC- Zeichensatz festhält, in dem der Dezimalwert der Zeichen wie- ~ der einen anderen Wert hat. Wo wir schon beim Thema sind ... |H»nter$rundinlo| Die Unicode-Zeichen haben natürlich nicht mehr Platz in einem Byte (was auf 256 Zeichen beschränkt ist), also in char. Daher wird für solche Zeichen statt auf char auf den breiteren Typen wchar_t zurückgegriffen. Die Größe von wchar_t wiederum hängt davon ab, wie die Compilerbauer diese implementiert haben (meistens mit 2 oder 4 Bytes). Bei der Verwendung von solchen breiten Zeichen musst du ein L vor das Zeichen stellen (bspw. L'A’). 72
Neben den für dich sichtbaren Zeichen gibt es noch Zei- chen, die zwar nicht dargcstdlt werden, die aber trotzdem etwas bewirken, Solche Steuerzeichen werden mit einem umgekehrten Schrägstrich (auch Backslash genannt), gefolgt von einem Zeichen mit fester Bedeutung geschrie- ben. Bekannter Vertreter Ist bspw. das Auslösen einer neu- en Zeile mit dem Zeichen ’ \ nf. Zeichenfolge Bedeutung akustisches Signal \b Rucksch rilVßackspace \f Seitenvorschub \n Zeilenende \t Tabulator (horizontal) \v Tabulator (vertikal) \ddd oktale Zahl (bspw. = ESQ \xhh hexadezimale Zahl \uXXXX \UXXXXXXXX für ZeKhen eines größeren Zei- chensitzes (bspw. Unicode) Ein paar mal habe ich dir ja bereits den Brocken mit den UTF-8-Zeichen hingeworfen. Und da ich eben wchar_t beschrieben habe, was ja auch nicht so recht was Gesehenes Ist, weil eben die Länge nicht exakt spezifiziert ist und cs somit wieder Probleme bei der Portierung auf anderen Rechnern geben kann, möchte ich dir kurz die drei neuen Unicode Kodierungen UTF 8, UTF-16 und UTF 32 vorstellen, welche in 0+11 eingeführt wurden. Dafür wurden sogar eigens für UTF-16 der Typ char 16_t und für UTF-32 der Typ char32_t zwei neue Typen hinzugefügt. \ Verwenden kannst du solche Unicode- Zeichen (Unicode-Codepunkt) mit der Zeichenkombination \ubzw, \U. welche du in einer String-Literale einbettest. Hinter \u muss eine 16-Bit Hexadezi- malzahl stehen, wohin gegen hinter einem \U eine 32-Bit Hexadezimalzahl Kodierung 1>P String-Literal UTF-8 char u8"Ein UTF-8-String" UTF-16 char16_t u"Elne UTF-16-String” UTF-32 char32_t U”Ein UTF-32-String" erwartet wird. Hier drei solche Beispiele, welche alle \u00f 6 verwenden, wel- ches dem Umlaut ö entspricht: cout « u8"Schr\u00f6dinger\n,r; // UTF-8 cout « u”Schr\u00f6dinger\n"; // UTF-16 cout « U1’Schr\u00f 6dinger\n" ; // UTF-32 Eine praktische UTF-8-Tabelle findest du hier: http://www. utfS-zeichwtabtlle. de/ Willst du hingegen direkt nach einem bestimmten Unicodezeichon suchen, hilft dir diese Webseite weiter: http //www. isthiithingon.org/unkode/indeit.php >t« C*« 73
Zeichen fUr die Welt So, nachdem du die Grundlagen zu den Zeichen kennengelernt hast, ist es nun an der Zelt, dass du hiervon welche auf die Welt loslässL Daher darfst du jetzt endlich auch wieder deinen teuer erworbenen Rechner anwerfen und ein paar Zeilen mit Code ein- tippen: *1 Hier wird jeweils einmal das Zeichen 'A1 In einA und das Zeichen 1 rZ‘ in cinZ gespeichert find ude < tos C reacn> und dann mithilfe des using namespace scd; «-Operatorsnach COut geschoben und auf dem ßiIrisch rm ausgegeben. Int main() { char elnA • ’A’, elnZ. ’Z'j *1 *2 Mithilfe des Ausdrucks int: (eh) kannst du den ganzzahligen Wert der Zechen 1A' und tZ1 auf dem Bildschirm ausgeben. cout « einA « « einZ « ’\n’; cout « int(einA) « ’\t' « int(cinZ) « 1\n char cout return 0; A65 = 65> Z9D = 90; « A65 « B\t' « Z90 « ' Das ist eigentlich mit Unicode schon pas *3 Anders hemm funktipn»crt dies genauso Hier ww»rd in den chaIT-Variablen A65 rirr gartj/ahligr Wert 65 und an Z90 der ganrzahllgc Wert 90 ubcrgcbefi □er Wert 65 ist auf der ASCH-Tabcllc der Buchstabe A und der Wert 90 ist em Z AllerdingsseUt diese An, ein Zeichen ah ganzzahligen Wert /u verwenden, voraus, dass auf dem Rechner siert bzw. im Gange, Unicode Ist ein Zei- chc-nsatz, der versucht, weltweit alle bekannten Zeichen in einem Zeichensatz zusammenzufassen. Das ist gar nicht so einfach, allein wenn du an die unzäh- ligen Zeichen in der chinesischen oder der ASCII-Zekheiwate lauft Ist dies nicht der Fall, wxd irgendein Zeichen aingegeben. weithin eben diesen dezimalen Werl im ZeichensaU hat Bern EBCDIC-Zeichensatz wäre bspw. 65 gar nicht belegt und 90 das Ausrufezeichen (1 >. koreanischen Schrift denkst. Und da eben die enorme Menge an Zeichen keinen Platz in einem char haben, welches ja auf 256 Zeichen beschränkt ist, kannst du hierfür char 16 toderchar32 tverwen den, mit denen du 65.536 und mehr als 4 Milliarden Zeichencodes unterscheiden kannst. Früher hat man dazu wchar_t verwendet, was aber eben mal 2 oder 4 Bytes groß ist. 74 C4pit*i »«ci
(Wvlirl Denk daran, ein char oder wchar_t selbst speichert keine Zeichen, sondern letztendlich auch wieder nur Ganzzahlen, die ihre Bedeutung erst mit dem auf dem Rechner befindlichen Zeichensatz erhalten. Einzelne Zeichen wie ’A‘ sind letztendlich nur symbolische «i//wchar t Konstanten für den ganzzahligen Wert des Zeichens aus dem Zeichensatz des Rechners. K<nJe bearbeiten) wchar_t ist kein einfacher 1-Byte-Typ mehr, weshalb der Stream cout diese Zeichen nicht mehr verarbeiten kann. Wenn du breite Zeichen verwenden willst, musst du diese durch die w-Streams schieben. In deinem Fall wäre das also wcout statt cout. Nun Ja. Schrödinger! Das Thema mit den Zelchensäizen mag dir am Anfang wie der Turmbau zu Babel vorkommen. Aber mittler- weile sieht es gar nicht mehr so schlimm aus. Spätestens seil dem C++11-Standard Ist es mit den neuen Typen charl6_t und char32_____t und der Unicode-Welt erheblich einfacher geworden, weil hier mit UTI-8, UTF-16 und UTF-32 gleich drei Unicode-Kodierungen unterstützt werden. <*Btypen 75
Erste »Zeichen“ für eine Pause Schrödinger ist frustriert, er hätte nicht gedacht, dass er sich um die Verwendung von einfachen Zeichen überhaupt einen Kopf machen muss. Im Augenblick hat er das Gefühl, dass er die Kontrolle über die Zeichen verliert. ’Na gut, Schrödinger! Fassen wir nal nur die wichtigen Fakten .zu zusaaaen, daait du das Ganze wieder klarer siehst: w Zum Speichern einzelner Zeichen kannst du Chat verwenden. char selbst speichert keine Zeichen im eigentlichen Sinne, sondern sucht das passende Zeichen aus einem Zeichensatz auf dem Rechner anhand eines dezimalen Wertes aus. " Einzelne Zeichen können zwischen einzelnen Anführungszeichen oder als Ganz* zahl mit dem Wert des Zeichens aus dem Zeichensalz verwendet werden. » Die Verwendung von einzelnen Zeichen wie T T' Ist im Grunde nur eine symbo- lische Konstante für den ganzzahligen Wert im Zeichensatz des Rechners. w C+* schreibt nicht vor, welcher Zeichensatz verwendet werden soll. Zwar kannst du fast sicher sein, dass der ASCII-Zeichensatz auf deinem Rechner unterstützt wird, aber bei der Verwendung von Zeichen darüber hinaus (bspw. Umlauten) wird es dir oft passieren, dass auf dem einen Rechner alles glattgehr. wahrend auf einem anderen Rechner Zeichensalat ausgegeben wird. Solltest du breitere Zeichen als char benötigen, findest du mit wchar_t einen Typ dafür. Anlog musst du dann auch die Streams für breite Zeichen verwenden (wcout. weerr, wein). C4+11 unterstützt mit UTF-8. UTF-16 und UTF-32 drei Unicode-Kodierungen. Für UTF-16 wurde der Typ char 16_t und für UTF-32 der Typ char32_t einge- führt. Die neue Unicode-Kodierung ist natürlich wchar_t vorzuziehen. 76 i>«ci
*1 Kein Fehler! De r Variablen B wird der Inhalt von VjnaWc A (also ein 1 A 1) zugewiesen char A ’ A’; char B A; *1 char C « D; *2 char E 66; *3 char F - 333; *4 char G = 1 /nr; *5 (Itaftahc Au<cjb-e] Bevor du deine wohlverdiente Pause mit deinen WoW- Freunden verbringen kannst, will ich noch, dass du die Fehler im folgenden Codeausschnitt findest IZeUen char A 1A1 । char B = A; char C « D; char E 66; char F = 333; char C 1/n*; cout « A « 1 1 « B « • 1 « C « 1 ’«£<< 1 « F « 1 ’ « G; •2 Fehler! Hier wurde versucht, an C eine Variable mit dem fleze-ichner D zu überweisen. welcher rwcht existiert D.i et keinen solchen N.imen gibt. war hie* wohl d.n Zeichen fD1 gemeint. ’3 Kein Fehler! Nach dem ASCII - Zeichensatz entspricht der ganzzahlige Wert 66 dem Zeichen ’ B ’ *4 Teilweise ein Fehler! Hier wird on Überlauf produziert und die Ganzzahl verwendet, welche der entsprechenden B«t-DarsteWng entspricht De/Compiler sollte dich allerdings davor warnen. ’5 Hier ist der SchrAgslach verkehrt herum Statt eines BacksUshs (\) wird hier ein normaler Slash </) verwendet
Wenn du stundenlang WoW spielst oder wenn du dich in der realen Wert zurecht- finden willst, braucht es gewisse Grenzen. In deinem konkreten Fall kommst du ohne räumliche oder zeitliche Grenzen nicht aus. Stell dir mal ein Leben ohne diese beiden Grenzen vor! Unmöglich! So ist es auch mit unseren Datentypen, die auch nicht Ober- irdisch und damit an bestimmte Grenzen und Größen gebunden sind. Genau genommen hängen diese Grenzen von der Aneinanderreihung einzelner Bits ab. welche die Zustände 0 und 1 darstcllen können. Dadurch lassen sich verschie- dene Werte darstcllen. Keine Sorge, ich fasse mich kurz. Nimm als Beispiel den Typ Chat. Du hast bereits erfahren, dass char 256 verschie- dene Werte darstcllen kann (egal, ob es jetzt mit oder ohne Vorzeichen implemen- tiert wurde). Diese Werte ergeben sich aus den acht aneinandergereihten ein- zelnen Bits (2 hoch 8 = 256). Das ist in etwa wie bei einem Zahlenschloss mit acht Stellen, bei dem jede (Bit-)Stelle den Wert 0 oder 1 haben kann. Hierbei gibt es 256 verschiedene Möglichkeiten, das Schloss zu knacken. Wie dieser Wert letztendlich Interpretiert wird (Zeichen, Ganzzahl, Gleitkommazahl oder Wahrheilswert), gibst du erst mit dem Datentyp an. Je mrär BtH Jtne4ninJer- fereiM *ind. deito gr&ter und die rfiritellbuen Werte, oder je gröRer d« $pir- «h*e»n, deit« mehr Geld pas»t 78
(Aufitunxl Beachte bitte, dass 1 Byte nicht zwangsläufig aus 8 Bits bestehen muss, auch wenn alle das vielleicht immer behaupten und es bei deinem und den meisten anderen Rechnern der Fall sein dürfte. Es gibt z. 8. Rechner, bei denen ein char mit 32 Bits implementiert ist. Sicherlich willst du jetzt endlich die Grenzen der einzelnen fundamentalen Typen wissen? Ehrlich gesagt, Ich kann sie dir nicht genau nennen, weil Dinge wie die Größe abhängig davon sind, wir diese implementiert sind. Anstatt dir hier also eine l iste mit typen aufruzlhlen. wie sie vielleicht sein könnten, bekommst du lieber Steckbriefe zu den Typen: Wahrheitswert: bool Da bool nur zwei Zustände speichern kann (true/1 und f alse/Ok sollte eigentlich 1 Btt ausreichen, um einen Wert zu speichern. Allerdings Ist die kleinste adressierbare Einheit eben 1 Byte, weshalb bool meistens mit dieser Große implementiert ist. Es ist aber durchaus möglich, dass bool aufgrund besserer Zugriffsgeschwindigkc it dieselbe Große wie die Prozessorarchitektur (22 oder 64 Bits) besitzt. Zticten: char. wchar_t. char 16_t und char32_t Ük Große von C-4-s-Ob^ekten wird immer als Vielfaches von der Große eines char angegeben, char ist in der Regel immer 1 Byte groß und kann sonnt 256 verschiedene Zeichen darstellen. Genügend für den ASCII-Code und auch deutsche Umlaute. Oer Typ wchar_t für breitere Typen {bspw. Unicode} ist gewöhnlich mit 2 oder 4 Bytes Größe implementiert. Denk daran, dass es sich bei den beiden Typen trotzdem um Ganzzahlen handelt. Besser als WChar_t für Unicode-Zckhen sind natürlich die neuen in C**11 einführten Typen char16_t und char32_t welche zur Zeichendarstellung von UTF-16 und UTF-32 verwendet werden, weil diese beiden Typen wesentlich portabler sind. So garantiert char!6_t eine Breite von mindestens 16 Bit wie auch char32_t mindestens eine Breite von 32 Bit garantiert. Bei WChart ist dies nicht gegeben! Ganzzahlen Der natürlichste Typ für Ganzzahlen ist int, weil dieser gewöhnlich zur Große der Ausführungs- Umgebung passt. Bei den handelsüblichen 32-Bit-Rechnern sind dies somit 4 Bytes. Bei 16-Bit- Systemen s«nd cs nur 2 Bytes. Bel den anderen Ganzzahltypcn gilt, dass char genau 1 Byte, «hort mindestens 2 Bytes und long mindestens 4 Bytes breit ist. Somit kannst du dich auf folgende Reihen bezüglich der Größe verlassen: 1 == char <= short <= int <= long (<= bedeutet «sf Weine/ oder gtertti) bie C***tiaslscyp«n 79
Gleitkommazahlen Um cs kurz zu machen, d>c Gleitkommatypcn sind rech! komplex, und auch hier Izwt sich relativ schwer Vorhersagen, wie breit diese auf deinem System sind. Häufig ist float mit 4 Bytes, double mit 8 Bytes und long double mit 10 oder gar 16 Bytes implementiert. Hierbei kannst du dich wiederum auch nur auf folgende Reihenfolge verlassen: float <= double <= long double {<= bedeutet ut Werner öder " * O Die Byte Größe der fundamentalen Typen auf deinem Rechner kannst du mit dem sizeof-Operator ermitteln. Einfach den Datentyp zwischen Klammem stellen, und der Operator liefert die Byte-Größe für den Typ zurück. Zum Beispiel kannst du dir sicher sein, dass folgende Ausgabe immer den Wen 1 zurückgibt: cout « sizcof(char) « endl; *1 •1 =1 Byte (ctiar iu immer 1) / J yti Sc£x0hf aber ich wollte eigentlich nicht wissen, wie viele Byte? ein solcher Typ hat, sondern eher welchen Wert ich bspw. in einen int speichern kann oder wie viele Bits ein char tatsächlich auf neinem Rechner hat? Die Antwort auf deine Frage findest du in der Spezialisierung des Templates (=Schab)one) numeric_limits in <liniits>. Gewöhnlich findest du don fürjeden fundamentalen Datentyp eine Spezialisierung. Der Großteil der Elemente in <llmits> dient dazu, Gleitkommazahlen zu beschreiben. Am meisten dürfte dich wohl interessie- ren, wie du den maximalen oder mini- malen Wert eines fundamentalen Typs ermitteln kannst. Hierzu ein kurzer Über- blick zu einigen Elemenifunktioncn, mit denen du bestimmte Informationen zu Typen in Erfahrung bringen kannst (TYP musst du natürlich durch deinen fundamentalen Typ ersetzen): 1 Hier bekommst du den maximalen Wert von TYP »rück. '2 D.isgibl dir den kleinsten Wert von TYP ruruck numerlc_limits<TYP>: :max() *1 nutneric_lIntits<TYP>:: tnin () ‘2 numeric_limits<TYP>: zdigits *3 nutnertc_limlCS<TYP>::is_slgned ‘4 *4G<b1 true zurück, wenn dein TYP ein Vorzeichen hat. Anwnjtcn tjl der Wcft false -3 Damit bc’<onim$t du die Anzahl der Bits von demem TYP zurück. 80 Üdphtl DtCl
The Final Frontier Da du jetzt weißt, dass die unendlichen Weiten deines Rechners nicht so unendlich sind, sollst du natürlich auch wieder etwas in die Praxis entsteigen und ermitteln, wie weit du auf deinem Rechner gehen kannst. Lerne deine Grenzen kennen! If irthchf AuigAbr] Finde heraus, wie bei dir der minimale und der maximale Wert von int lauten und wie viele Bits und Bytes ein char auf deinem Rechner hat. (Schwierige Au1gabe| Optional kannst du auch noch versuchen, herauszufinden, ob char mit oder ohne Vorzeichen eingebaut wurde. •1 Hier bekommst du über die f Irmentc raax ( ) und min ( ) den maximalen und minimalen Wert von int auf deinem System zurück. O*ese Spe/ial<$ie«urig liegt in de« Regel auch für alle Anderen Typen vor. #inelüde <iostream> lincludc <Liiaits> using namespace sed; *2 Das gibt dn die Anzahl der Bits van einem char juf deinem Rechner ruiück. Int mainO ( // Einfache Aufgabe: cout « nuE>eric_^limics<inc>: ttnax() cout « nuineric_liniits<int>: :min() cout « nuEeric_liTnlts<char>; zdiglts « endl; cout « sizeoffchar} « endl; *3 —. _ M *3 Mithilfe des S iz CO f - Operators kannst du den Speicherverbrauch in Bytes für «einen Typen ermitteln. Im konkreten Fall wird char immer den Wert 1 junxkgeben (egal vnr wie Bits 1 Byte au< deinem System hat). die <»•a&4xiityp*n 81
II Schwierige Aufgabe: bool Vorzeichen - nutneric_lfmits<char>:: i$_signed; if( Vorzeichen true ) { als signed inplcmcnticrt\n"; 0; « "char ist cout > eise { cout return « "char ist als unsigned implementierten"; verstehe Schablonen auch. Ich muss quasi nur in den spitzigen Klammern meinen Typ reinsetzen, von dem ich die Grenze auf eine« System kennenlernen will. TT^TTYtj 32 c«»k«i »eci (ttotkr] Beachte bitte, dass es abhängig vom Typ und der Implemen- tierung noch viel mehr Elementfunktionen In gibt, über die bestimmte Informationen ermittelt werden können. Gerade bei den Gleitkommatypen stehen enorm viele Informa- tionen zur Verfügung. ’4 Kt char nut einem Vöriekhen implementHrt. £ibt das f ipment is_signed den Wen true iurtkk. Amonsien wüd falsc zurück- l ‘V Rückgabewert speicherst du in Vorzeichen l>zhrr war dir Aufgabe ah Schwierig ; '<-kr nrwichnrl, weil bis dato if-Überprüfungen noch <i*ht behandelt wurden, Hier Wird praktisch überprüft, ob(if) Vorzeichen gleich truc ist Arwonsten (eise) »st Vorzeichen gleich f al SC Es wird nur der enls^wechende AnweisungsMock mit der Ausgabe angeführt. wektie* wahrheitsgemäß iutriHt Aut denen liegt mittlerweile ein Fluch. Weil cs sich hierbei urn Relikte vergan- gener Zeiten handelt, seihest du in Programmen die Finger davon lassen. Ruhende Geister soll inan nicht wecken. Nicht umsonst bietet dir schließlich C++ mit eine Spezialisierung in Form von Templates dafür an.
Gut, dass c£\_r so &tc?ZC es Grenzer gibt... Okay, Schrödinger, da ich deine Gedanken lesen kann, kriegst du hier noch einen Überblick über die fundamentalen Datentypen mit ihren .üblichen' Wertebereichen und Größen an die Hand. Zunächst die Ganzzahhypen. bei denen du auch gleich den Wahr heiuwert und die Zeichemypen (dieJa eigentlich auch Ganzzahl- typen sind) vorfindest: lyp Speicher I Wertebereich bool 1 Byte 0 oder 1 bzw. false oder true char 1 Byte -128 bis +12? bzw. 0 bi* 255 signed char 1 Byte -128 bi* *127 unsigned char 1 Byte 0 bi* 255 wchar_t 2 oder 4 Byte* abhängig von der Implementierung short 2 Byte* -32-768 bH *32.767 unsigned short 2 Byte* 0 bi* 65.535 int 4 Byte* -2,147.483.648 bi* *2.147483.647 unsigned int 4 Byte* Obi* 4.294.967 255 long 4 oder 8 Byte* wie int oder -9.223.372.036-854.775.808 bi* *9.223.372.036.854.775.807 unsigned long 4 oder 8 Byte* wie unsigned int oder 0 bi* 18.446.744 073.709 551615 »ia 83
/ dass int der natürlichste Typ zur BREITET DER PROZESSOR* ARCHITEKTUR ist. Bei einem 16-Bit-Rechner hat int also 2 Bytes. Bei einem 32-BIT-RECHNER gibt mir der sizeof-Operator 4 Bytes zurück. Wie ist es aber jetzt bei den neuen 64-BIT-RECHNERN? Bei mir werden da nach wie vor 4 Bytes zurückgegeben. Ich hatte aber 8 Bytes gemäß deiner Aussage erwartet. Okay, du hast es so gewollt! Das liegt am verwendeten Programmiermoddl P64, wobei die Breite von int auf 32 Bits be- lassen wird und stattdessen der Typ long 64 Bits breit ist. Der Zeiger ist hierbei ebenfalls 64 Bits breit. Das Modell dürfte hei vielen Linux- und Unix-Sy Siemen (also auch dein Mac} zu finden sein. 64 Bit-Windows verwendet hierbei oft das AAodell LLP64, in dem sowohl int als auch long 32 Bits breit bleiben, aber die Zeiger auf Adressen 64 Bits lang sind (•lur? Luft holen*), Für einen 64-Bit-Typen nimmt man dann den frisch standardisierten Typen long long. Auf 32 Bit-Rechnern wie Windows, Mac, Linux wird das Modell ILP32 verwendet, bei dem alles nur noch auf 32 Bits beschränkt ist. Das einzige Datenmodcll. bei dem int, long und der Zei- ger tatsächlich 64 Bits breit sind, ist im Augenblick ILP64, mit dem ich aber noch nicht das Vergnügen einer Bekanntschaft hatte. tZHt«Q Zum besseren Verständnis. Das L steht für Long. das P für Pointer (Zeiger) und I für Integer. Die Zahlen 32 und 64 sprechen für sich. Ergänzend zur bereits gezeigten Tabelle mit den Ganzzahltypcn wurden mit dem neuen C++11-Standard (und z.T. schon lange davor) noch folgende Ganzzahl' bzw. Zeichentypen hinzugefügi: Typ Speicher WcrtcbcTctch char16_t 2 Byte! abhängig von der Implementierung char32_t 4 Byte* abhängig vor» der Implementierung long long 8 Bytes -9_223.3 72.036.854.7 75.808 bis *9,223.372.036.854.775,807 unsigned long long 8 Bytes Obis 18.446.744.073.709.551.615 Jetzt fehlt dir nur noch der Überblick über die Gleitkommatypen: iyp Speicher Wcrlebenckh float 4 Sytes 1.2E-38 3.4E+38 double 8 Bytes 2.3E-3O8 1.7£*308 long double 10 (oder Bytes 3,41-4,932 1.1£*4.932 84 tut]
Arbeiten mit Zahlen Von Zahlen verweht Schrödinger erfährt, dass man in C++ auch rechnen kann (wofür {a Computer ursprünglich gebaut wurden). Obwohl er überhaupt kein Freund von Zahlen Ist und auch Mathe In der Schule nicht gerade sein Paradefach war, bei t er sich durch das Thoma. Glücklicherweise stellt er fest, dass auch Gleitkommazahlen mit mathematischen Schwächen zu kämpfen haben und auch beim Rechnen mit Zahlen einiges drunter und drüber gehen kann. Da Schrödinger gerne experimentiert, mischt er auch einfach mal die Zahlen durch und erfährt, dass es dabei gewisse Regeln gibt, nach denen die Typen umgewandelt werden. Schnell bemerkt Schrödinger auch, dass hier Möglichkeiten bestehen, mit deren Hilfe er die Typen zwingen kann, etwas zu werden, wo- für diese eigentlich nicht vorgesehen sind. Allerdings stellt er selbst schnell fest, dass es nicht Immer so gut Ist, eine Schraube mit dem Hammer einzuschlagen, sondern oftmals besser, die dafür vorgesehenen Werkzeuge zu verwenden.
Nachdem du deinen Prozessor mit nutz- losen Befehlen gelangweilt hast, wollen wir uns in diesem Abschnitt mit einem Thema beschäftigen, für das dein Rechner gebaut wurde: dem Rechnen. Egal, ob du jetzt einen Text schreibst oder WoW spielst, dein Rechner macht trotzdem nichts anderes als rechnen, ein ganzes Prozessorleben lang. Ich weiß, kein schö- nes Leben für ein Stückchen Silizium. Okay, jetzt bitte wieder volle Aufmerksamkeit. Zunächst wirst du die einfachen Operatoren für die Grundrechenarten kennen- lernen, die du auch meistens unter dem Namen arithmetische Operatoren findest, ich gehe mal davon aus. dass du die Grund rcchenanen bereits kennst, daher findest du in der folgenden Tabelle nur noch dir Darstellung der Operatoren: Operator Bedeutung 4. Addition _ Subtraktion Den Modulo-Operator darfst du nur mit Ganzzahlen verwenden. Gleitkommazahlen sind damit nicht erlaubt. Der Operator luhrt wie / eine Division von zwei Ganzzahlen durch. Nur gibt dieser Operator ak Ergebnis den Rest der Division zuruck. Beispielsweise wurde die Berechnung von 11Z 3 das Ergebnis 2 zurückgeben. well das eben der Rest der öiwslon 11 / 3 ist. Bei den Grundrechenarten handelt es sich um binäre (lateinisch für bi = zwei) Operatoren, die jeweils an der rechten und linken Seite je einen Operanden benötigen (.Operand [Operator] Operand). Der Operand kann dabei eine Variable oder ein Wert sein. Dass eine Verwendung, wie z.B. wert 1+wert 2, alleine als Befehl häufig keinen Sinn macht, wirst du die Berechnung gewöhnlich auch irgendwo sichern wollen, wes- halb du das Ergebnis dieser Berechnung irgendwie verwenden musst, zum Beispiel indem du es mit dem Zuweisungsoperator (-) einer Variablen zuweist. Natürlich bedeutet das jetzt nicht zwangsläufig, dass rin solcher Ausdruck ohne eine Zuweisung falsch wäre. Am besten siehst du dir die folgenden Zeilen an. in denen die Anwendun- gen der Grundrechenarten mit C++ demonstriert werden: 88 Vic*
’1 In dickem Fall wird zw.ir eine Addition durchflchihrL aber das f rgebnis nirgendwo verwendet odc* zwischcngcspcichcrt. Zwar ist n Zeil syntaktisch kein Fehler. sie hat aber deinen Effekt und »st daher nutzlos. Int ergebni«; int wertl « 11, wertZ = 3; "2 Hier dasselbe nochmals, nur «rtrd jetzt die Be- rechnung der $ubV.ililBO<i aB Ausdruck über den Operator« nach cout geschoben und auf dem Bildschirm ausgegeben. Das Ergebnis selbst wird allerdings nirgendwo (zwischen-^gespeichert ”3 Hier wird das Ergebe $ der Divrslon von wert 1 durch wert2 in der variablen ergebnis gespeichert und m der nächsten Zeile ausgege- ben. wertl + wertZ; *1 cout « wertl - wertZ « endl; *2 crgebnls - wertl / wcrtZ; *3 cout « ergebnis « " - Restt 11 « werclZwert2 « endl; '4 ergebnis = ergebnis * 2; '5 cout « ergebnis « endl? *S Natürlich dürfen juch Zahlen und Variable bei den arithmetischen Berechnungen gemischt verwendet weiden, wie diese Multiplikation demonstrieren soll. ’4 Oa die Division vor wert 1 durch wert2 in der Zr lr zuvor einen Rest ergibt, kannst du diesen (Rest). w hier gezeigt. mithilfe des AVodulo-Opcrator (Z:- berechnen lassen Im Beispiel wird diese Berechnung nicht zwischengespeichert und direkt über den Ausgabeoperator an cout geschickt. [Zttttll Natürlich hält sich C+ + auch an die Punkt- vor-Strichrechnung, welche vorschreibt, dass Multiplikationen und Divisionen vor Additionen und Subtraktionen durchgeführt werden. Somit ergibt 10 *10*2 gleich 30. Willst du zuerst die Strichrechnung durchführen, kannst du Klammerungen ver- wenden. weil diese einen höheren Rang bei der Operatorenrangfolge haben. Somit würde (10 + 10) * 2 gleich 40 ergeben, weil hierbei zuvor die Berechnung in der Klamme- rung durchgeführt wird. Erweiterter ______________________________ firundrechenzuwelsung^^QQ^^^^^B Berechnungen, wie z. B. könntest du mit folgender kürzeren Schreibweise verwenden ergebnis - ergebnis • sume summe ♦ drei; summe + drei; Diese Form steht für alle fünf Gnmdrechcnoperatorcn / und Z zur Verfügung. irb»it*n ait 17
Dein Prozessor ist eigentlich ein richtiges Arbeitstier, und du solltest ihn nicht mit den stundenlangen Leerlauforgien (engl. M/e) bestrafen, welche das Betriebssystem regelmäßig veranstaltet. Daher ist es jetzt an der Zeit, dass du auch mal ein Programm schreibst, was der CPU-Langeweile ein Ende setzt. (ElAfiche Auigob+J Schreib ein Programm, welches das Volumen eines Schuh- Icartons ausrechnet- Verwende double als Datentyp. cm als Maßeinheit für den Karton. Rechne die berechneten Kubikzentimeter am Ende in Liter um (LOOOcc 1 Liter). Futter für den Prozessor Zeit, ein richtig nützliches Programm für die Schuhindustrie zu schreiben Hierzu eine mögliche Musterlösung des Programms 38 vielt
int TnainO linclude <iostreara> using namespace std; double voluioen; double laenge, die Typen fur die Berechnung •1 breite (cm) •2 «S *2 das Volumen der Schubser tr h’rl in Kubik/rntimcte! ausrechnen Dititf SehuhkurtOrt dB wö&l eher lüi* di* Htirrftwfll {ödtf-f lur grd-iChE (CA. GrölW 40 bil 4fi| Schrödinger ist total aus dem Häuschen, dass ihm das Programm gelungen ist, und ruft schon mal bei seinem Chef an, um mit ihm die Planungen für künftige Schuhkartons zu besprechen. Er hat jetzt schon eine Menge Ideen! cout « "Voluoen : " « volumen « " Litcrln"; '5 return 0; ‘2 die Maße dei Schuhscb^rhtel m rm eineeben out « "Länge ein » laenge; cout « "Breite (cm) in » breite; ‘2 out « "Höhe (cm) ein » hoehe; *2 $ das trgebnrs auf dem Bildschirm jusgeben volumen - laenge * breite * hoehe; volumCn/ -1000; *4 hoehe; d.n Eqjrbn<s durch 1.000 teilen (i'.OOOcc • i U1cr) mithilfe drs erweiter- ten Grundrcchcnzuwcisungsoperatws 2 Das Programm bei seiner Ausführung s Baer (cm) (cm) (cm) ./schuhkarton 34 20.5 12.5 Dieter Länge Breite Höhe Volumen : S.7125 Liter Dieter Baer $ Arbeiten ait 89
Kopfrechnen Schrödinger ist immer noch sehr gut drauf. Endlich macht ihm das Programmieren mehr Spaß, weil er erstmals etwas Nützliches pro- grammieren kann. Natürlich führt er am Abend das Programm seiner Freundin am Laptop im Wohnzimmer vor. Die ist selbstverständlich an allem interessiert, was mit Schuhen zu tun hat. So hat sie Schrödinger schließlich kennen gelernt, g vor den Doppelgängern Ich muss dich unbedingt noch darauf hin weisen. dass es zu den binären Operatoren +, - und * noch unäre Gegenstücke gibt. Die unären Operatoren haben im Gegensatz zu ihrem binären Pendant nur eine einseitige Beziehung zu einem Operanden. Operator 1 Binär 1 Unlr + Addition Liefert das positive einer Zahl zurück. Liefert das negative einer Zahl ivnkk Verweisoprrator. Lasst sich nicht In dieser Spalte beschreiben. • Subtraktion Multiplikation •fr ‘2 Da', unäre Plus vor der Zahl könntet du dir hier sparen, weil auch ohne Angabe* dc5 Vorzeichen der Wen 100 .ri pOSitiV iujgv.viCSCfi würde. Natürlich haben diese unären Operatoren einen höheren Rang als die binären Gegenstücke. Ja. auch da gibt es eine Hierarchie, die nötig ist, damit folgende Berechnungen ohne Probleme möglich sind: ’1 Dank des unären Minuf vor der Zahl erhält negativ den Wert -100. int negativ = -100; *1 int positiv - *100; ’2 cout « negativ * positiv « endl; *3 cout « 1000 ♦ -100 « endl; cout « -100 + *200 « endl; *4 ’3 Dass -100 plui *100 gleich 0 ergeben, sollte jeder wissen, der regelmäßig sein Konto überaieht 90 Uo'.ftl vIC*
kann nicht mit Gleitkommazahlen verwendet werden. Itiafach* Aufgabe] Finde die Fehler, die in den folgenden Zeilen gemacht wurden? Kettel) double oster • 12.5, ei • 3 + 0; double osterei « oster X ei; int kokosnuss - 10; kokosnuss-«-5; double oster - 12.5, ei - 3*0; *1 double osterei = oster I ei; *1 int kokosnuss Ä 10; "2 Kokosnuss-=-5; *2 *2 Auch das »5t kein Fehler h*er wird über den erweiterte^ G<undrcchcnzuweisungsopcrator der Wen von kokosnuSS mit dem negativen Wert -5 subtrahiert. Wenn du das Ergebnis ausgibst, entpupp! sich die Kokosnuss bei den menten ah echte Kopfnuß (10 - -5 = 15k Probier es ruh<g mal am ... J *4 Diese Berechnungen sollen demonstrieren, dass die uniren Operatoren eine höhere Priorität besitzen ?un.icb$t werden hier nämlich die Werte mit den Vcczechen ausgewertet und erst dann die arithmetische Berechnung mit den brniren Operatoren durchgeführt. Hier die Lösungen zur Aufgabe (BrlöhMurtj/Ldrturtgl So. das hast du gut gemacht, und wenn du die Kokosnuss nicht verstanden hast, ist das nicht weiter schlimm. Hier solltest du einen Mathematiker um Rat fragen. Du darfst jetzt zur Belohnung deiner Freundin ein neues Paar Schuhe kaufen, nachdem du ihr mit deinem Wissen über Schuhkartons regen Appetit darauf gemacht hast. Arbeiten B-it ZlMtn
Dieser Abschnitt hier ist unbedingt nötig, weil es beim Rechnen mit C++ einfach ein paar Grenzen gibt, über die du Bescheid ' wissen solltest. -^4, HfiTniwri«. Keine Grund zur Sorge, Schrödinger, ich behandle nur kurz, was du grundlegend wissen musst, falls du Probleme beim Berechnen deiner Schuhkartons bekommst. Würdest du bei Ganzzahlen, z. 15. mit dem Typ int. bspw. numeric_ limits<int> : :max( ) + l verwenden, so würde dein Compiler meckern, dass ein Überlauf einer ganzzahligen Konstanten statt findet. Betrachten wir mal folgenden Codeausschnitt: int overflowOl 21474B3647+1; int one = 1; *2 int overflow02 2147483647+one; ‘1 Einen soktien Integer-Ob+rUul tollte dein CornpikT bemerken *2 "2 Hier drückt der Compiler leider schon m. ein Auge zu und hat nichts ?u beanstanden, obwohl hier aixh ein Überlaut statlfindct Wie kommt es also, dass eine Berechnung von z. B. zwei Signed int-Typen wie 2.000.000.000 + 2.147.483.647 zum Ergebnis -147.483.649 führt? Zur Vereinfachung, damit ich nicht so viele Bit-Stellen verwenden muss, soll der Typ uns igned short verwendet werden, der meistens mit 16 Bits implementiert ist. In der folgenden Abbildung solltest du erkennen, warum eine Berechnung von 65.535 (dem maximalen Wen von uns igned short) plus 1, nicht 65.536, sondern 0 ergibt. Wenn du die 16-Bit-Darstellungen der beiden Werte betrachtest und dann zum Ergebnis in der letzten Zeile siehst, ftllt dir zwar auf, dass das Ergebnis der Bit-Darstellung auf der Tafel ja tatsächlich dem Wert 65.536 entspräche. Aber da für uns igned Short nun mal nur 16 Bits zur Darstellung des Wertes zur Verfügung stehen, wird das 17. Bit einfach ahgeschnitten (hier wird es vom Hubschrauber ahgeholt). und als Ergebnis bleiben 16 Bits mit 0 stehen und somit ist auch der Wert 0. 92 vlt*
Bn «*nrm Ubetlavf wrtdrn frihLuh d« -Ub*f5leh<ndt<i’ äbgtuhrtiHfrt Ähnlich Ist dies auch bei vorzeichenbehafteten Werten, Bei einem slgned short von 32.767 (maximaler Wert) plus 2 kommt nicht 32.769, sondern -32.767 heraus. Wenn du dir diese Berechnung ebenfalls auf der Tafel in der Bit-Darstellung ansiehst, wirst du sehen, dass hier zwar kein Oberlauf stattfindet, dass aber bei vorzcichcnbchaf- teten Zahlen das höchste Bit (hier das 16.) verantwortlich für das Vorzeichen ist, wes- halb hierein Vorzeichenwechselstattflndei. ß*im Utanchmtc*» der Grtrwen tadel bei *ö/- zekta*behah<1*n Zahlen keift Übediuf tun, »andern das Vorzeichen wird ftlrtdert. lAchtun^] Das Bild vom Hamslerrad ist im Prinzip gar nicht so falsch, um diesen Über- bzw. einen Unterlauf zu verstehen. Aber, du solltest wissen, dass eine solche Bereichsüberschreitung eigentlich ein Fehler ist und du dich nicht darauf ver- lassen darfst, dass alles wieder von vorne beginnt! Nicht umsonst warnt dich der Compiler, wenn er eine Bereichsüber- schreitung während der Übersetzung mit einem konstanten Wert erkennt! Arbeiten ait Z«h|*n 93
Okay, ich fasse es kurz: Gleitkommazahlen können nicht immer exakt dargestellt werden! Das Thema Gleitkommazahl ist eigentlich Teil der theoretischen Informatik, und du solltest dich bei Bedarf selbst darüber informieren. Damit Nichtverstandenes erst recht nicht verstanden wird, hierzu noch eine kurze Notiz tz*'"l] Gewöhnlich werden die Gleitkommazahlen im E-Format (+-f.fffE+-e) geschrieben. So wird bspw die Zahl 1.0 wie im E-Format mit + 1.000E+0 beschrieben oder -0.006321 mit -6.321E-3 und die 0.0 mit +0000E+O. Wenn du jetzt 2/6 + 2/6 ausrechnen willst, kommst du auf das Ergebnis 4/6. Glück für deinen Mathelehrer, aber hier geht das Dilemma mit Pünktchen und Rundungsfehlern schon los 2/6 ist im E-Format gleich +3 333E-1. Addierst du jetzt +3.333E-1 mit +3.333E-1. ist das Ergebnis +6.666E-1 (bzw. 0.6666). Wieder gut für deinen Mathelehrer, aber leider falsch, denn 4/6 sind im E-Format +6667E-1. aber nicht wie berechnet +6 666E-1 Mit derartigen Rundungsfeh- lern haben viele Computer ihre Probleme. Damit dir diese Rundungsfehler in deiner Laufbahn nicht zum Verhängnis werden, solltest du Folgendes beherzigen: ** Da selbst eine Zahl wie 1,0 Rundungsfehler enthalten kann, solltest du niemals Gleitkommazahlen auf Gleichheit überprüfen (also nicht die Operatoren != und == verwenden). Stattdessen solltest du immer auf größer gleich oder kleiner gleich (die Operatoren: <= und >=) testen. *• Wenn du eine Software erstellst, mit der Geldbeträge verwaltet werden, solltest du auf keinen Fall Gleitkommazahlen, sondern Ganzzahlen verwenden. Bei einem Geld- betrag von 99,99 furo solltest du dann mit 9.999 Curo-Cents rechnen. Wenn du trotzdem unbedingt Gleitkommazahlen verwenden willst, musst du dich umfassender damit auseinandersetzen. «4 Vic*
Schwächen offenlegen In der Praxis ist cs nicht immer einfach, auf solche Überläufe entsprechend zu reagieren. Trotzdem ist das ein Thema, mit dem du dich als Programmierer Irgendwann befassen musst. Da uns hier jetzt mindestens zwei Seiten zur Verfü- gung stehen, wollen wir diesen Platz ein wenig für Denksportaufgaben nutzen. Es kann sein, dass dir dabei einiges recht seltsam und kryptisch vorkommt, aber du bekommst hierdurch ein Gefühl dafür, mit Zahlen zu jonglieren. |5chwi«f igt AwIgAbrl Ohne dass du bisher die Mittel der Operatoren kennengelernt hast, versuch mal rein logisch herauszufinden, wie du einen Ganzzahlen- übcrlauf mit zwei vorzeiehenloscn Variablen gleichen Typs ermitteln könntest, die zusammenaddiert werden, Wenn du dich mittlerweile mit den Typen vertraut gemacht hast und du Denksport- aufgaben liebst, sollte dir die Lösung gar nicht so schwer gefallen sein. Im Grunde brauchst du nur zu überprüfen, ob die Summe der beiden Ganzzahlen kleiner als der größte der beiden Werte Ist. Programmtechnisch kann dies wie folgt aussehen unsigned short wertOl 65535, wertOZ = 1; unsigned short summe; bool overflow (sunme^vertOl+wertOZ) < wertOl; *1 if( overflow - true ) { cout « "Achtung! Bereich wurde überschritten^"; M jHerfvud iuiuch$t aufgrund der höhefc^rriorität der Khmmcrung die Sumn^tcfechrvcL 0«e Summe des E/gcbntaes ufr p-nJcn wir daran, ob der Wen kleiner Xiis wert 0 1 ist. was ja nidit sem kann, r wenn addiert wird. Triftt dies zu. ist overflow gleich true fazid kein ÜbecUul statt. ist overflowgleich false ~ He Aiberprüfst du, ob overflow glei<h true (-Überlauf) ist. Ist dies der Fall, wird tfrt Beispiel der entsp/e* chendr T«t ausgegeben arbeiten Bit Zahlen 95
JSchwi« rife Avfgjbd Zwar kannst du auch diese Aufgabe als normaler Anfänger nicht lösen, aber denk mal nach, was du theoretisch machen müsstest, um zu ermitteln, ob bei einem Vorzeichen behafteten Typ das Vorzeichen gewechselt wurde? Prima gegoogelt, Schrödinger! Um das allerdings durchzufuhren, müssen wir schon auf einer sehr tiefen Ebene anseizen und Bit-Operatoren dafür verwenden. Mit Bit-Operatoren kannst du direkt auf die binäre Darstellung der Einsen und Nullen von Ganzzahlen zugreifen. Keine Sorge, ich werde das jetzt nicht aufrollen, weil du so etwas in der C++-Praxis wohl eher selten brauchen wirst. Trotzdem auch hier ein Codeausschnitt *1 Ifi d^ser kryptischen Zeile wird überprüft, ob das höchste Bit von wertO 1. wir hier mi| numeric_liinixs<short>: :diglts den Compiler selbst setzen lassen, gesetzt Ist An S ignO l wird dann ontweder t ruC (VorzekhervBitgesetzt)oder falsc^ (nicht gesetzt) zugewiesen. *3 Nachdem die Berechnung durchgcfohrt wurde, machen wir dieselbe Überprüfung nochmals mit dem höchsten Bit des Ergebnisses der Berechnung. short ergebnis werrül + wert02; bool sign02= (ergebnisk (l«nunieric_liEDits<short>: :d short wertOl 32767, werv02 I; bool signO1=(wertOl&(l<<numeric_limits<short>::digits)); r1 if( signOl 1= signOZ ) {-^ cout « "Achtung! Vorzeichen^ljurdc gewechselt\n *3 Jetzt vergleichen wir be^Je Vorzeichen. Sollten die beiden Booleschen Werte signO 1 und sign02 nicht gleich <! =: sein, wurde das Vorzeichen bei der Berechnung auf jeden Fall gewechselt (Achtung! Das Prinzip der schreitung bzw. ei nur mit zwei gleiche besitzen. Sobald du eine Typ verwendest, als dein Zi das Prinzip nicht mehr. So wird zwar z. 8. mit dem unsigned short-Wert 65.534 plus dem konstanten Wert 65.537 der Bereich einmal überschritten, da aber das Ergebnis 65.535 lautet, wird dies bei der Überprüfung nicht mehr bemerkt uellen Überprüfung einer Berekhsüber- Vorzeichenwechsels funktioniert natürlich . die falls einen korrekten Wert konstanten Wert oder aufnehmen kann, funktioniert 96 vl£*
Mir raucht der Kopf Gleiches gilt auch für das .Hamsterrad' eines Vorzeichenwechsels. Wird ein solcher Vorzeichen wechsel dauerhaft wiederholt, solltest du dich auch hier niemals darauf verlassen, dass schon alles wie immer sein wird und stimmt. *• Bel einer Ober- bzw. Unterschreittmg des Wertebereiches hängt das Ergebnis von der Implementierung ab. Konkret: Tritt eine Bercichsübcrschreitimg auf, kann nicht 100%ig vorhergesagt werden, was dabei herauskommt. Ich weiß, dieses Thema hat cs in sich, und du wirst vielleicht auch zunächst sehen damit in Berührung kommen. Bei Bedarf musst du dich ohnehin intensiver damit befassen, Wichtig ist hier jetzt nur zu wissen, dass du bei der Verwendung von Zah- len nicht kopflos arbeiten solltest und dich nicht darauf ver lassen kannst, dass schon alles gut gehen wird. Auch wenn es dir ein wenig kryptisch vorkommt, merk direinfach folgende Stich punkte: (ZMIO] Findet eine Überschreitung bereits zur Übersetzungszeit statt, bekommst du eine Warnmeldung vom Compiler, bei der du nicht wie bei den Arbeits- anweisungen deiner Freundin einfach die Ohren durchlüften kannst. Ein Punkt noch, und dann kannst du eine rauchen gehen. Das Thema mit der Genauigkeit von Gleitkommazahlen wurde hier noch gar nicht richtig aufgerollt. Es sollte dir klar sein, dass die Genauigkeit von Gleitkommazahlen auch nur endlich ist und sein kann. >lt Z«M*n 87
In der Praxis stehen dir dafür normalerweise Gleitkommazahlen für einfache Genauigkeit mit 32 Bits (float) und doppelter Genauigkeit mit 64 Bits (double) zur Verfügung. Eine nochmals erweiterte Genauigkeit ist meistens mit 80 oder sogar 128 Bits mit long double implementiert. Ohne Jetzt in Versuchung zu geraten, mehr als nötig zu erklären, müssen in dieser Gesamtlänge das Vorzeichen, der Exponent und die Mantisse untergebracht werden. Dadurch, dass die Mantisse begrenzt ist, entsteht irgendwann einfach eine Ungenauigkeit. In der Praxis kommt hierbei meistens das genormte Gleitkommasystem IEEE 754 zum Einsatz. Hierbei wird der Nachkommateil in der Mantisse gespeichert. Bei einfacher Genauigkeit sind dies meistens 23 Bits und bei doppelter Genauigkeit 52 Bits für den Nachkommateil. Bei der erweiterten Genauigkeit ist diese Breite nicht exakt definiert. Teste dazu einfach mal folgenden Code aus *1 Hier wird zweimal dieteJbe Berechnung durchgcführt, einmal wird dat Ergebnis in float nd einmal in double gespeichert. float fval - 1000.12 I la2345; *1 double dval » 1000.12 / 1.2345; "1 2AMprccision() kannst du die Genauigkeit der Gleitkommazahl für die Ausgabe ml COUt eimiellwi. im Beispiel wollen wir zehn Stehrn haben. cout.precisiont10); *2 cout « fval « endl; II 810.1417847 *3 cout « dval « endl; II 810.1417578 ‘3 *3 Ole Aufgabe demonstriert das Problem mit der Genauigkeit von Gleitkommazahlen float Irt hterbei mit s>cbcnstHliger Genauigkeit am Ende Som4 könnte float nicht zwischen den zwei 1*/aS Sc// tc/i ( *'>/*—. *1 float/ double oeit-t long |Noti<rem’Ut>en| f loat-Werten 810,141711 und 810,141799 unte<«heiilen double hingegen rtt häutig mit einer 15’itelligen Genauigkeit implementiert und bat daher noch etwas mehr Spielraum Reicht dir die Genauigkeit von den Gleitkommawerten nicht mehr aus, die dir C++ anbietet, solltest du dich nach einer Festkomma-Arithmetik umsehen. Iteiolwiun^lofvngl Jetzt hast du dir eine Zigarette verdient. c-toit.i vlt*
Abgesehen von den Grundrechenoperatoren ist C++ kein Mathe-Ass. Mathematische Funktionen sind in die Standardbibliothek ausgelagert. Der Vorteil. $o etwas zu machen. Ist natürlich der. die Programme, die keine mathematischen Funktionen brauchen, etwas schlanker zu hallen. Wer komplexe Zahlen mit einem Real und einem imaglnarteii benötigt, wird mit der Standard-Templatc-Klasse complcx bedient. Wer nicht weiß, was komplexe Zahlen sind, wird diese ohnehin nicht benötigen, weshalb ich mir eine wissenschaft- liche Beschreibung dazu sparen kann, Komplexe Zahlen kannst du mit den Gleitkommatypen float, double und long double verwenden. Weil es ein Template (Schablone) ist, wird der Gleit kommatyp zwischen spitze Klammem gestellt: Ünclude <complcx> '1 complex<double> eoe ine Komp lexeZahl(-l .5, 3.3>; *2 ‘1 Diese Header- datei ist nötig, wenn du komplexe Zählen verwenden willst. |Abli<e] "2 Hier wurde die komplexe Zahl incincKücnplcxcZahl vom Typ double mit dem Realteil -1.5 und dem Imaginärteil 3.3 initialisiert. Hier haben wir .lußrrdrm auch gleich die neue €♦♦ 11-tnbalsie* fungssyntox verwendet’ KLappl dis bei dir nicht musst du die geschweiften Klammern gegen runde austauseben. Hierein Notizzettel mit häufig verwendeten Funktionen: Wenn du nur den Real- oder den Imaginärteil einer komplexen Zahl benö- tigst, findest du die Elementfunktionen real() bzw, lmag() dafür. Neben den Grundrechenoperatoren +. * oder / und Operatoren für Gleichheit oder Ungleichheit sind natürlich auch noch spezielle komplexe Funktionen implementiert, die man im all täglichen Leben von komplexen Zahlen häufig Funktion Wozu normt) Gibt das Quadrat des Betrages zurück. abs ( ) Gibt den Betrag der Wurzel aus HOrm( ) zuruck. COtl j ( ) Gibt den konjugierten Wert zurück. arg () Gibt den Winkel in Polatkoordinaten zurück. polar ( ) Gibt d*e komplexe Zahl zu Poiorkoordinaten zurück. verwendet. Arbeiten .(( Z«h|«n 09
Wenn du auf der Suche nach den alten, aber soliden Knackern aus C-Zeiten bist dann wirst mit <coath> fündig. Darin findest du trigonometrische, hyperboli- sche, exponentiale, logarithmische und noch andere mathematische Funktio- nen. Alle Funktionen von <cmath> kön- nen mit float, double und long double verwendet werden. Ich weiß, hier werden Dinge nur der Vollständigkeit halber angerissen, nicht dass jemand mal behauptet, mit nativem C++ könne man nicht rechnen. Allerdings Ist die Mathematik ohne hin eine Nebensache in dem Buch. Wenn du solche Dinge nicht brauchst, kannst das auch überspringen. Und wenn du cs brauchst, wirst du am linde des Buches über diesen Abschnitt hier lachen. iz*«i«n Einen Zettel habe ich noch für Mathe- matiker, und zwar die Notiz zu Vektor- berechnungen, wo C++ die Template- Klasse valarray aus der gleichnamigen Headerdatei zur Verfügung stellt. Warum valarray und nicht vector als Kias- senname verwendet wurde? Nun, der Name vector wurde schon für eine andere Klasse verwendet und war somit vergeben. 100 tuittl vielt
Keine Ahnung, aber Ich verwende es trotzdem Sicherlich freust du dich jetzt schon aut' den Einsatz der komplexeren Mathespiclcrcien in C++? Nicht? Egal, das muss jetzt trotzdem sein. Schließlich soll cs auch Leute geben, die nicht nur Soft- ware für eine Schuhkanon-I'abrik entwickeln. |NOlu| Ich hohe, du erwartest dir von diesem Buch keine Funktionsreferenz zu den mathematischen Funktionen. Das wäre stinklangweilig. Hierzu gibt es Google oder Bing, und die meisten Entwicklungsumgebungen bieten da auch schon eine Referenz mit an. Notfalls kannst du dir ja auch ein solches Buch kaufen, wo auf über 1.000 Seiten die Tabellen mit aufgelistet werden. Unbedingt brauchen tust du es aber nicht. Kurz und schmerzlos ein Listing, welches die komplexen Zahlen demonstrieren soll: '2 Einfache arithmetische linclude <loscream> Berechnungen^ehen linclude «complex* natürlich auch m.t komplexen Zahlen, linclude <ctnach> using namespace std; int mainO ( complex<double> lex001(2+3» 3.4); *1 complex<double> lex002(l.l> 2.2); *1 "1 Hier wird jeweils ei ne komplex« Zahl erzeugt Würdest du bei der Erzeugung keine Werte verwenden, wurde automatisch eine komplexe Zahl mit (0.0 + 0.0 0 erzeugt Auch die Ausgabe über den Stream cout ist im Sundard- Au^dbefoinxiL (1.2, 3 .4) implementiert. complex<double> vollex « lexOOL + lex002; ‘2 cout « vollex « endl; *3 •4 H^r wird jewetk de* ße-al- ur*d ImagmArteil ermittell und wieder double real = vollex.real(); *4 Iur Erzeugung eines rwuen double imag vollex. imagO ; *4 komplexen Typs vetwendet. Irbtll«» >lt Z«M*n 101
complcx<double> neulex(real, imag); cout « neulex « endl; U > > *4 Hier wird jeweils der Real« und ImaglnArtell ermittelt und wieder zur Erzeugung eines newn komplexen Typs verwendet double pt - 4.0 * atan(1,0); ^5* < cout « pi « endl; # * e » double phase « pi/4.0; // 45° double magnitude - 2«0; complex<double> vollplex polar(nagnitude» phase); >6 cout « vollplex << endl; < complex<double> inlex; cout « "Komplexe Zahl eingeben ein » inlex; *7* • • • e cout « inlex « endl; return 0; > “6 Hier Ussen wär eine komplexe kartesische Zahl aus den Polarkoordmaten berechnen. 7 Auch die Eingabe -.xxi komplexen Zahlen im Format (1.2, 3.4) ist implementiert. •5 0«e Punktion atan( ) Ist Teil de* alten cmath-ö^iotbek. In diesem Beispiel lassen wir Pi berechnen [Schwieaje Aur^abt) Eine Aufgabe, die es in sich hat und wohl nicht von jedermann gelöst werden kann, Aber kannst du eine Umrechnung einer kompfexen Zahl in Polarkoordinaten berechnen? Tipp: Für die Magnitude gibt es die Funktion abs () und für den Winkel die Funktion arg( >. 102 Uc‘.tel Vic»
Hier die Kurz-und-schmerzlos-Lösung double pl 4.0 * atan(l.O); double phaseOl pi/4.0; // 45* double magnltudeOl 2.0; II Umrechnung aus Polarkoordlnaten complex<double> vollplex polar(magnltudeOl* phaseOl); '1 Diesen Vorgang kennet du ja bereits vom ersten Usling. bc> dem du aus den Werten der rwm.itcn Gleit punkttypen eine Umrechnung aus den Polarkoordinjten zu komplexen Zahlen berechnet tast. ff Umrechnung, in Polarkoordinaten double phase02 - arg(vollplex); double magnitudc02 • abs(vollplex); cout « phase02 « *' ” « magnitude02 « endlf (Cod* ’2 Der umgekehrt© Wog. um aus der komplexen Zahl wieder m Polarkoordiruten zurückzurechnen, ist dank der beiden Funktionen arg ( } und abs ( ) denkbar einfach Hinweis Willst du außerdem noch den Winkel in Grad aus der Phase (im Beispiel phase02) berechnen lassen, brauchst du nur folgende Rechnung zu erstellen: double winkel=phase02/pi*180; *rb«lt«n Bit !«hl*n 103
Am Ende der Mathewelt Na. damit hättest du die Welt der Mathematik in C++ hinter dir. War doch ein Kinderspiel? Die Well der Mathematik ist ziemlich faszinierend. Kommt dann noch die Programmierung dazu, klingt das Ihr viele wie Musik, itJ W/r€ ,<14 I Du hast air jetzt lange genüg neine Mathe-Schwachen unter die Nase gerieben. Ich bin schon froh, wenn ich »eine Jahresabrechnung für das Finanzamt zusammengerechnet habe! Ach, komm schon, so schlimm war cs auch wieder nicht. Zum einen habe ich das Thema Mathematik und C++ ja nur kurz angerissen und zum anderen kannst du dabei eigentlich nur Neues dazulernen. Wenn du also Berechnungen in C++ durchführen willst, stehl dir Folgendes zur Verfügung: 3 die üblichen Grundrechenoperatoren für die Basisdatentypen 3 komplexe Zahlen, die aus dem Real- und Imaginärteil bestehen 3 die Klasse valarray für komplexe Vektorberechnungen 3 die alten C-Funktionen aus <cmath> für alle reellen Zahlen Echte Hardcore-Mathematiker brauchen natürlich noch weitaus mehr als das vorliegende Angebot. Denen empfehle ich, die boost-Bibliothek von http://www.booit.ofg/ nachzuinstallieren. Aber bitte erst, wenn du C++ selbst richtig gelernt hast. / I. . . Damit es nicht ganz so heftig wird, bekommst du auch noch eine J 1 w einfachere Aufgabe. (EiaUchf Aulgibr] . Berechne die Diagonale des Bodens einer Schuhkarton-Schachtel. Die Formel und die Maße dazu findest du in der folgenden Zeichnung. Zum Ziehen der Quadratwurzel kannst du die Funktion $qrt() von <cmath> verwenden. Die Funktion erwartet eine reelle Zahl als Parameter und gibt auch eine solche vom selben Typ zurück. 104 C«pK«l MIC*
Wri waNe* die Diagonale der $<hvh»ch>cMH mit 24*17 <cm haben. Okay, hier eine einfache Musterlösung dazu, in der du auch die Länge und Breite (war ja nicht gefordert) der Schuhschachtcl eingeben kannst: linclude <iostream> linclude <cmath> using namespace scd; int mainO < double laenge, breite, cout « "Länge (cm) 1 ein » laenge; *1 cout « "Breite (cm) : *1 die Werte finleven . ein » breite; *1 b2... und enup<echend dte Oagormlc de^ R«ctrteck^ .luwechnen diagonale"(lacngc*laenge) + (brcfcc*brcite); *2 diagonale-sqrt (diagonale); *2 cout « "Diagonale " « diagonale « " cm" « endl; return 0; Dein PregfiAffl b*r feiner Auifuhruitg lfcrloh,nun£] So, jetzt hast du es geschafft. Zur Belohnung darfst du weiter an deiner WoW-Karriere feilen. Dieter öaer $ ,/diagonal Länge (cro) : 24 Breite (en> : 17 Diagonale 29*4109 cz Dieter Baer $ Arbeiten (£ ZeM*n 105
Sicherlich stellst du dir jetzt die Frage, was denn bei den Berech- nungen oder im Allgemeinen passiert, wenn du die grundlegen* den Datentypen miteinander mischen willst. Fs ist nämlich durchaus möglich, unterschiedliche Operanden miteinander zu kombinieren. Allerdings solltest du die- se Schicksale nicht dem Zufall überlassen und als Herrscher über dein Programm selbst in der Lage sein, zu steuern, was bei einer Umwandlung geschieht, und vor « allem, wann es geschieht. Hierbei kannst du die Geschicke dem Compiler über- lassen oder selbst eingreifen. n Wenn du dich nicht um die Typ- umwandlung kümmerst, macht das der Compiler Für dich. Hierzu einige Beispiele. bei denen eine solche interne Typumwandlung von deinem Compiler vorgenommen wird: •w Bei Zuweisungen wird der Wert, der auf der rechten Seile des Zuweisungs- operators (=> steht, automatisch in den Wen konvertiert, der auf der linken Seite steht. Du kennst zwar noch keine Funktionen, aber erwartet eine Funktion als Argument einen bestimmten Typ. und du übergibst stattdessen einen anderen Typ an diese Funktion, wird auch hier das Argument automatisch in den erwarteten Typ der Funktion konvertiert. Umgekehrt ist es natürlich genauso. Gibst du einen anderen Typ aus einer Funktion zurück, als im Funktlonsprototyp angegeben wurde, wird der mit return zurückgegebenc Wert automatisch in den erwarte- ten Rückgabetyp konvertiert. "• Bei arithmetischen Berechnungen und Vergleichen von zwei verschiedenen Typen wird automatisch immer der kleinere Typ in den größeren Typ umgewandelt. 106 Gc.tii vic*
unii. Tethusdrwche wi unUnchirdhchfm Typ werden vöhi Cowipikr immer ia den ftbcMthohttftn Typ umge windelt. {Achtung Natürlich sollte dir klar sein, dass eine unbedachte Standard-Typumwandlung auch zu Verlusten von Informationen fuhren kann. 5o kann bspw. der Nachkommateil bei int einer Konvertierung einer reellen Zahl wie double in eine Ganzzahl wie int nicht mehr erhalten bleiben. Selbiges gilt bei Überschreitung von Wertebereichen, wenn der Quelltyp nicht mehr in den Zieltyp passt. Auch bei unterschiedlichen Vorzeichen in einem Ausdruck kann es zu unerwarteten Problemen kommen. Neben der impliziten Typumwandlung gibt es noch eine bestimmte Form der Umwand- lung, die du als Programmierer häufig gar nicht mitbekommst. Hierbei handelt cs sich um die Promotion eines Typs, die der Compiler durch führt. Wenn du die Abbildung oben nochmals ansichst, fallt dir sicherlich auf, dass eine Typumwandlung bevorzugt auf dir Typen int, unsigned int und double hinausläuft. Naja, ich habe die- se Typen hier auch nicht umsonst gesondert hervorgehoben. alt Zahl*» 107
Eine solche integrale Promotion eines Typs wie bool. char. s igned char oder short auf int bzw. unsigncd char oder unsigned short auf uns igned int und dann noch die Gleitkomma Promotion von float nach double wird meistens zur Vereinfachung von arithmetischen Operationen , durchgeführt. , Heißt das, egal was ich jetzt für Typen für die arithmetischen Berechnungen verwende, diese werden letztendlich ohnehin mit int. unsigncd int oder double durchgeführt? Und was genau wird hierfür wenn vereinfacht? Im Grunde wird in der Tat fürjede arithmetische Operation eine Promotion auf int, unsigned int oder double durchgeführt, wenn nicht rin Operand einen noch größeren Wertebereich aufweist (bspw, long oder long double). Die Verein- fachung des Typs dient natürlich der besseren Performance für dein Programm. Da bestimmte Typen an dir Wortbreite {den natürlichsten Typ deines Prozessors) angepasst werden, sind die Berechnungen hiermit am schnellsten. Mit dem Hammer Last Hut nicht ganz hoffnungslos (magst du auch kein Denglish?) kannst du die Schraube auch mit dem Hammer reinschlagen. Sprich, Typen, die nicht passen und bei denen du es nicht dem Compiler überlassen willst, sic umzuwandeln, und bei denen du auch weißt, was P * du tust (hoffentlich), kannst du selbst passend machen. (Achtung] In der Praxis solltest du. wenn möglich, eine Typumwandlung vermeiden Sicherlich kann eine explizite Umwandlung mal notwendig oder sinnvoll sein, aber in den meisten Fällen kann auf eine explizite Typumwandlung durch ein Überdenken (oder Vorausschauen de Planung) des Codes verzichtet werden. Wenn du trotzdem ausdrücklich eine Typumwandlung durchfuhren willst, musst du diese Umwandung mit einem sogenannten Cast-Operator kennzeichnen. In C++ stehen dir hierzu vier solcher Cast-Operatoren zur Verfügung. Hier meine persönliche Rangliste der vorhandenen Operatoren (es wird immer der Ausdruck expr in den Typ TYP umgewandelt): 1. Explizite Typumwandlung wenn möglich vermeiden! 2. Static_cast<TYP>(expr) - Der Cast Operator wird für eine typische Standard-Typumwandlungen verwendet. In der Praxis wird empfohlen, übliche Typumwandlungen damit zu kennzeichnen, dass sic auch implizit vom Compiler so gemacht würden. Zum einen lässt sich dadurch deutlicher erkennen, dass hier eine Standard-Typumwandlung durchgefÜhn wird, und zum anderen erkennt man auch, dass du weißt, was du tust (sollte zumindest so sein). 108 vltt
3. dynatnic_cast<TYP> (expr) Dieser Operator ähnelt static_ cast, nur findet die Überprüfung zur Laufzeit statt, wenn der Compiler bei der Übersetzung den Typ noch nicht erkennen konnte. Den Operator an dieser Stelle genauer zu erklären, würde dir noch nicht viel bringen, well dir noch einiges an Wissen fehlt. Aber für das Protokoll: Der Operator wandelt Zeiger oder Referenzen zwischen abgeleiteten Klassen um. Also, der Operator funktioniert nur mit Zeigern auf polymorphen Klassen mit mindestens einer virtuellen Elementfunktion. 4 const_cast<TYP> ( expr) - Objekte, die mit const gekennzeichnet wur- den. lassen sich normalerweise nicht mehr ändern. Mit diesem Operator könntest du vorübergehend const außer Kraft setzen. Aber in der Praxis kann ich dir davon nur abraten, einfach weil nicht garantiert werden kann, dass das Speicherobjekt deswegen geändert werden kann. Und das. was du bestimmt nicht willst, ist ein Programm, das nur vielleicht macht, was du willst oder zu wollen meinst. .Nützlich* kann dieser Operator sein, wenn du konstante Zeiger oder Referenzen auf konstante Daten an eine Funktion übergeben musst, welche eigentlich keine konstanten Daten erwartet. Aller- dings ändert das dann nichts daran, dass in der Funktion selbst wiederum eine Ände- rung der Daten zu einem unerwarteten Verhallen fuhren kann. 5. reinterpret_cast<TYP> (expr) - Diesen Cast wirst du wohl in der Praxis eher selten brauchen. Damit kannst du quasi die wildesten lypumwand- lungrn durchführen. Dieser Operator arbeitet auf einer ziemlich tiefen Ebene der Programmierung, weshalb das Ergebnis der Umwandlung häufig auch abhängig von der Implementierung ist. In der Praxis macht dieser Operator daher nur Sinn, wenn du mit rohen Bits und binären Bytes arbeiten willst. IZmrf] Erwähnt werden sollte hier noch der function-scyle Cast von C++, Dieser Cast erinnert ein wenig an den alten C-Cast, nur dass dieser mehr an einen Funktionsaufruf erinnert; | float fval - 123.123; I int ival int(dval); I II function-scyle Cast Aber auch hier greift man viel eher auf die neuen I Cast-Operatoren (hier z,B auf static_cast<>). (Achtunfl - Lass auf jeden Fall die Finger von alten C-Casls ä la (typ)ausdruck. Das ent- spricht dann schon nicht mehr mit dem Hammer, sondern eher mit der Brechstange auf die Schraube zu hauen. Die Umwandlung mit alten C-Casts macht oft bereitwillig viel Unfug mit, was zu schwer auffindbaren Fehlern führt. Außerdem erkennt man an den alten C-Casts sehr schlecht, was da gerade umgewandelt wird. >lt Z«M*n 109
Warum man sich nicht auf JEDEN Typ einlassen sollte... Natürlich wollen wir cs uns nicht nehmen lassen, hier ein paar Typen in der Praxis durch den Flcischwolf zu drehen und zu schauen, was dabei herauskommt: Arbeit für den Fleischwolf fincludc <iostrcam> using namespace std; int funktion{ int wert } { int iwert wert * wert; rcturn iwert; > int mainO { > '1 Hier siehst du eine implizite Konvertierung br> einem Funkti- onsaufruf Zunächst übergibst du der Funktion einWert () einen double-Wed als Argument, obwohl diese Funktion einen int-Wert erwartet. Der Compiler übernimmt die Konvertierung implizit für dich. Die Funktionen selbst wirst du spater noch krnnenlrrnrn. double einWert 9.99; cout « funkeion(einWert) « endl; short sWertOl cout « sWertOl short sWertO2 « sWert02 cout long cout char cout 32767; *2 ♦ 1 « endl; Z/-32768 ’S sWertOl + 1; *3^* « endl; X/--32767 iWert sWertOl; *4 « IWert « endl;^M a - *A1; *5 « static casc<int>(a) end 11 double dwerc 123-123; int iwert » stacic_cast<int>(dwert); cout « iwert « endl; return 0; Da hier die Berechnung be< der Zuweisung gemacht wird, ist es votbei mit der integralen Ausweitung, und short enthalt einen Überlau- fenden Wert Quasi em Fehler »m Programm’ 110 unm VIC*
*2 Da« hier der short-Werl bei der Vorher nachdenken Kode be*rt»eiten) Alle Beispiele zeigen dir auch deutlich, dass du hierbei komplett hättest auf Umwandlungen verzichten können. Anstatt eine Umwandlung vorzunehmen, hattest du dich hier gleich von vor- ne herein auf einen einheitlichen Typ festlegen können. Ausnah- Ausgabe mittels Cout nkhi übetUulen Ht und -32.767 ausg»bt. verdank« du der integralen Ausweitung Promotion), die der Compiler automatisch bet der arithmeti- schen Berechnung durchgrfuhrt hat mc im Beispiel dürfte die Umwandlung von char nach int *5 gewesen sein, £me klassische Konvertierung von char in ein löt Hier wird der dezimale Wert des Buchstaben ’A' ausgegeben. f i Hier findet eine korrekte Konvertierung von short nach long statt. Da long eine gröfiete Breite als short hat, ist das Überhaupt kein Problem, Trotzdem Ware « hier deutlicher, wenn du static_casc verwenden würdest “6 Bei dieser Konvertierung von double nach int kannst du die Nachkommastelle nicht mehr retten. Abe* hierbei wurde zumindest mit dem 31at ic_caSt-Operator deutlich gemacht, dass wir das hier nicht aus Versehen, sondern mit voller ™ Absicht gemacht haben. arbeiten Bit ZaM** 111
Dor sanfte Typ An dieser Stelle angekommen, hast du jetzt drei Varianten der Typumwandlung kerincngelemt. Da wäre die implizite Typumwandlung, bei der man allerdings nicht immer sicher sein kann, ob der Programmierer weiß, was er tut, oder ob es eben ein Versehen war. Dann hast du die automatische Ausweitung (auch bekannt als Promotion) kennrngelcrnt, bei der der Com- piler klammheimlich die Typen der arithmetischen Berechnung zur besseren Performance des Programms optimiert, Indem er versucht, die Typen in eine Breite zu stecken, die dem Prozessor besser schmeckt. Und natürlich hast du erfahren, wie du mit Cast-Operatoren anzeigcn kannst, dass du zum einen doch weißt, was du tust, aber damit zum anderen auch recht brutal auf die Typen einschlagen kannst. Das Motto, wenn etwas nicht passt, wird es passend gemacht, solltest du speziell In der C+*-Programmierung überdenken und dir immer von vorneherein Gedanken machen, was für Typen du in deinem Programm wofür benötigst. Nimmst du dir nur 5 Minuten mehr Zeit für die Planung, kann dir das am Ende des Projektes sehr viel Zeit mit dem „Nachstricken" von Code ersparen. Ach ja, eines habe ich da noch für dich, und zwar das Mischen von vorzeichenlosen (unsigned) und Vorzeichen behafteten (signed) Typen. Hierzu macht man sich zunächst wenig Gedanken, weil Ja der Compiler auch In dem Fall mit dei auto- matischen Promotion bercitsteht. Der Compiler wandelt einen int Ausdruck dann entweder in den entsprechenden signed oder den ttnsigned-Typ. Allerdings solltest du dabei wissen, dass bei einer Konvertierung eines S igned-Typs in einen unsigned Typ nicht vorhersehbar Ist, was passieren wird. Wie bei einem Überraschungsei hängt dein Ergebnis dann vom Compiler ab, was du ja auf keinen Fall willst. [Anm. des Lektorats: Mensch, Bär, was für eine langweilige Seite ...] 1» vic«
lAtMung] Das Mischen von signed und unsigned (bspw. zum Vergleich) solltest du tunlichst unterlassen Normalerweise warnt dich dein Compiler ja schon vor, wenn er einen signed==unsigned-Vergleich entdeckt. Mischst du signed und unsigned trotzdem, kann es passieren, dass der Vergleich eines signed int-Wertes von -1 gleich dem unsigned int-Wcrt mit 4 294 967.295 ist, weil beide Bit-Darstellungen identisch sind. |EiAi<eh» Auigib*] Du hast jetzt sicherlich festgestellt, dass du bei vielen Umwandlungen auf static_cast verzichten kannst, weil der ( Compiler dir da ja automatisch mit einer impliziten Umwand- lung die Arbeit erleichtert. Warum solltest du trotzdem einfach Typumwandlungen mit static_cast umwandeln? Die Antwort könnte ziemlich umfangreich ausfallen, aber grundlegend solltest du hierbei mindestens Folgendes angemerkt haben: Du dokumentierst damit, was du hier machen willst (.Ich weiß genau, was ich da tue"). Dir Lesbarkeit wird damit eindeutig verbessert. Auch wenn dir zunächst das Konstrukt operator<T>(ausdruck) etwas kryptisch erscheinen mag, so gewöhnt man Sich recht schnell an diese Schreibweise. ** Fehler bei der Umwandlung von Typen werden so schneller gefunden, weil du im Quellcode nur nach den entsprechenden Cast-Operatoren (bspw. Static_cast) suchen musst. Und wer noch die alten C-Casts kennt, hier haben die neuen Operatoren spezielle Funktionen, und es gibt nicht nur einen Cast-Operator für alles. Damit kannst du dir sicher sein, dass deine gewünschte Umwandlung auch funktioniert. lrb«lt*n Bi« 113
|ti«fachr Aufgabe) Im folgenden Codeausschnitt hat der Programmierer ein sauberes explizites Casting eingebaut. Hast du etwas daran auszusetzen? long Iwert; cout « "Wert eingeben : "j ein » Iwert; short swert static cast<short>(Iwert); Jöt i der Anwender hier ei- nen long-Wert eingibt, der unter oder über dem Wertebereich von short liegt, gehen diese Informationen darunter BZW. DARÜBER VERLOREN, und der Wert in short ist dann ein ganz anderer. Automatische Typenableitung Neu mit C++11 wurde das automatische ?\blcitcn von Typen mit dem neuen Schlüssel- wort auto eingeführt. Damit kannst du auf die Nennung des Typen für deinen Bezeich ner beim Initialisieren verzichten. Der Compiler ist klug genug und weiß, was er mit auto val 1234; auto dal - 123.123; machen muss, Anhand des Intlallsierers erkennt der Compiler hier den Typen und macht intern daraus Int val - 1234; double dal = 123.123; Was sich hier viclcicht zunächst noch etwas trivial anhört, kann dir später bei der Ver- wendung von komplexeren Typennamen oder beim Durchlaufen von Behälter Klassen eine ziemliche Erleichterung darstcllen, weil du dich hier dann nicht mehr mit den Typen hcrumschlagen musst. IBt-löhMunjj'LiMtrrigJ Eine Belohnung wartet! 114 c.oit.l Vic*
—FÜNF— Rgene Entscheidungen treff en oder das Ganze nochmal bitte | Kontroll- strukturen Im wirklichen Leben het Schrödinger der- in C++ zelt nicht viel zu sagen. Ständig meckert seine Freundin an Ihm herum, was er anziehen soll, dass er den Eimer mit den Zlgarettenstummeln In den Müll werfen soll oder dass er sich besser ernähren muss. Zum letzten Punkt hat er sich zumindest breitschlagen lassen und Isst Jetzt seiner Freundin zuliebe Dinkelpfannkuchen, obwohl Ihm eigentlich Schnitzel mit Pommes am liebsten sind. Umso mehr freut sich Schrödinger, als er erfährt, dass er In C++ Uber Kontrollstrukturen selbst entscheiden kann, wo es langgeht. Ebenso fragt sich Schrödinger, wie er wohl Immer wiederkehrende Aufgaben durchfuhren soll. Es kann Ja nicht sein, dass er 99 mal den gleichen Code eintippen muss. Damit Schrödinger künftig nicht unendlich viele Codezeilen wie- derholen muss, lernt er verschiedene Schleifonarten kennen.
Bisher musstest du noch keine wichtigen Entscheidungen im [.eben treffen?! Dann wird es jetzt höchste Zeit, dass du das Heft in die Hand nimmst und für dein Leben geradestehst. Naja, ein biss- chen extrem, aber bei der C**-Program- micrung bestimmst jedenfalls du, wo es langgeht. Prima, das können wir ja gleich in einem theoretischen C++ Konstrukt als eine Verzweigung formulieren: if( freundin == da ) *1 ( dlnkelvollwercpfannkuchenessenl); > ' ’ Wenn dr ne Freundin d.i ist, dann werden die Anweisungen des Anweisungsblocks zwischen den geschweiften Klammem dahinter ausgefuhrt In diesem freispiel wird nur die Anweisung bzw Funktion dinkclvollwcrtpf<mnkuchcncsscn() aufgeruten. eise *2 < HähnchenHaniburgerEisEssen() ; > undweitergehtsO; *3 "2 Ist deine Freundin nkht da, werden ansonsten die Anweisungen zwischen dem Anweisungsblock hinter der e Ise Verzweigung ausgetührt, wo die Funktion HähnchcnHamburgcrEisEsscnf) aufgerufen würde. "3 Egal, ob deine Freundin jetzt da ist oder nicht, das Prog/.imm wird auf jeden Fall hinter den Verzweigungen weiter ausgeführt. Es sei denn, du brichst das Programm irgendwo in der Verzweigung ab. 116 C4g.fl FW
«SS / IZeHflJ Der alternative else-Zweig, der gewöhnlich immer dann ausgeführt wird, wenn bei einer oder mehreren if- Anweisung(en) keine Bedingung erfüllt wurde, ist optional. Du musst also kei- nen solchen eise-Zweig verwenden und kannst natürlich auch nur einen oder mehrere if-Zweige benutzen Allerdings kann ein eIse-Zweig nur verwendet werden, wenn mindestens ein if-Zweig vorausgeht. Freundin nicht da (Achtung] Einfache Frage: Da du den else-Zweig nicht unbedingt verwenden musst, könn- test du hier darauf verzichten und gleich Hühnchen- HamburgerEisEssenf) hinter dem if-Anweisungblock setzen? Reicht dir eine i f-Verzweigung nicht aus, kannst du mehrere solcher Verzweigungen hintereinander verwenden, indem du nach der ersten if Anweisung weitere eise if Anweisungen anftigst. Auch hier kannst du dann optional wieder als letzte Verzweigung ein eise verwenden. Oer PföfiiffwnablAtilplin Ibnwr HtenipUnl 1ü« SchtödiAftf HähnchenHamburgerEisEssen() Als Erweiterung zu deinem Beispiel: if( freundin da ) ( dinkelvollwertpfannkuchenessen(); } Control 1 Struktur«n jn <• 117
clsc if( kuchlschrank == leer ) ‘1 < pizzabestellen(); } eise ( HähnchenHamburgerElsEssen()j *1 Hier wurde unicr SpeisepUn erweitert. Sollte d»r erste if Bedingung (dass unsere f rcundm da ist) nicht zuUcffcn. wird d»C$C rwe t<? 8cd*ngüJig üb-crpruft. rumllch ob der Kühlschrank leer ist Trifft das zu, bestellen wir eine Pizza. Trifft diese zweite clsc if-Bedingung auch nicht zu. wird die C Ise Verzweigung ausgelührl. Der PrQ|$r*mnMbl*urpUn wurde mil eise If- Zweig utn-eanen .NöUaII- plarT e*welCerl Freundin nicht da Freunctm da Kühlschrank leer Dir sollte natürlich klar sein, dass bei einer solchen Verkettung von if-else if-else-Verzweigungen nur jeweils eine Verzweigung ausgefuhrt wird. Brauchst du mehrere Verzweigun gen. die vielleicht ausgeführt werden sollten, verwende einfach mehrere if-Verzweigungen ohne eise davor. (Hihfergrurtdirtl^l Die runden Klammern hinter if bzw. eise if musst du für den logischen Ausdruck immer angeben. Logisch bedeutet in C++ ganzzahlig. Daher kann jeder Ausdruck zwischen if einen be- liebigen numerischen Wert annehmen (genauer: nach bool konvertierbar sein). 0 bzw. false wird immer zurückgege- ben, wenn der logische Ausdruck falsch (bzw. unwahr) ist. und ein Wert ungleich 0 wird immer als wahr bzw. true inter- pretiert. !*bla$el Du kannst übrigens auch innerhalb der if-Verzweigungen wei- tere if-Verzweigungen verschachteln Allerdings erschwert dir ein solches Verschachteln von if-Anweisungen das Leben bei der Fehlersuche erheblich. Im Gründe ist ein tiefes Verschach- teln selten nötig, wenn du dir vorher mehr Gedanken zum Design deines Codes machst. Und noch ein Tipp: Ein sauberes Einrücken innerhalb eines Anweisungsblocks hat auch noch nie mandem geschadet. 118 taoktfl FOMF
Verzweigung, Abzweigung oder Kreuzung Zelt, den Essensplan für dich in die Praxis umzusetzen. Das folgende Listing demonstriert den Vorgang, den du im Büro zuvor durcligegangeii bist, in der Praxis. Die Ausführung zu verstehen, sollte dir Jetzt keine Probleme mehr bereiten: linclude <iostream> using namespace std; int mainO { bool freundin, kuehlschrank; cout « "Freundin da (l-ja/0-nein) : ein » freundin; cout « "Kühlschrank leer (l»ja/O«nein) : ”; ein » kuehlschrank; if(freundin true) { cout « "Zeit für Dinkelvollwertpfannkuchen!\n"; > eise if(kuehlschrank true) { cout « "Pizza bestellen!\n"; > eise < cout « "Hähnchen, Hamburger und Eis essenl\n”; } cout « "Nach dem Essen!\n"; return 0; > Das Programm bei der Ausführung Dieter Baer S ./essen Freundin da (l-ja/0-nein) : 1 Kühlschrank leer (l-ja/0^ncin) : & Zeit für Dinkelvollwertpfannkuchen! Nach dem Essen! Dieter Baer $ ./essen Freundin da (l=ja/0=nem) : & Kühlschrank leer (l-ja/0-nem) ; 1 Pizza bestellen! Nach dem Essen! I Dieter Baer S I tontrell Jtryl türm in <* 11»
Im Listing hast du den Vergleichsoperator=s verwendet, um zwei Spei ehe robjekte auf Gleichheit zu testen. Gerade bei if -Verzweigungen wird regelmäßig auf diese Ver- gleichsoperatoren zurückgegriffen, bzw. vieles könntest du ohne diese Operatoren gar nicht realisieren. Die Verwendung ist einfach! Rechts und links vom Vergleichsoperator stellst du jeweils einen Operanden. Der Rückgabewert eines solchen Vergleiches ist ein Wert vom Typbool. Ist der Vergleich falsch, wird 0 bzw. false zurückgegeben. Bei einem wahrheitsgemäßen Vergleich wird 1 bzw. true zurückgegeben. Ein einfaches Beispiel: *1 Hier wird false zurückgegcbcn. weil int wert Not 1, wert No 2 - 2; wcrtNol oicht gröfier als wcrtNoz ist. bool Wahrheit 1 = wertNol > wertNoZ; *1' bool Wahrheit2 • wertNol !• wertNo2; ‘Z. E:ür sinnvolle Vergleiche stehen dir natürlich eine Handvoll dieser Vergleichsoperatoren zur Verfügung. die du in der folgenden Tabelle aufgelistct siehst: '2 Oas ist Jetzt wahr, also true weüwcrtNol ungleich wertNo2 >st Operator | Wozu I Praxis Rückgabe < kleiner a < z true. wenn a kleiner als z. ansonsten false <= Meiner oder gleKh a <= z true. wenn a kleiner als oder gleich z. ansonsten false > grofter a > z true, wenn a großer als Z, ansonsten false >= großer oder gleich a >= z true, wenn a größer als oder gleich Z, ansonsten false gleich a “ z truC. wenn a gleich z. ansonsten false I- ungleich a I« z true. wenn a ungteich Z trt. ansonsten false Nein, viele Klassen, die du noch in deinem Programmiererleben kennenlernen wirst, haben diese Operatoren ebenfalls sinngemäß implementiert. So kannst du z.B. bei der Klasse String auch Zeichenketten lexikalisch miteinander vergleichen. Mil der Klasse String habe ich dir später im Buch noch ein Treffen vereinbart. Noch ein kleiner Hinweis: Wenn eine if-, eise if- oder else-Anwei$ung aus nur einer Anweisung besteht, kannst du die geschweiften Klammern 0 um den Anweisungsblock auch weglassen. Einen solchen Block musst du nur verwenden, wenn du mehrere Anweisungen zu einem Block zusammenfassen musst 120 c«»k«i Foxr
Wrong tum? (Spricht Bit vollen Mund:] ^T. Du weißt jetzt, wie du dein Leben selbst in die Hand nehmen kannst und eigene Entscheidungen treffen musst - zumindest in deinem Programmiererleben. Solche Entscheidungen wirst du relativ hluflg treffen müssen. Der Vorteil im Gegensatz zu deinem echten Leben ist natürlich, dass die digitalen Entscheidungen wesentlich logischer und einfacher zu fällen sind als die realen. CttAjf <?4tS C++ Ja, das geht! Dafür gibt es in C++ die logischen Operatoren, womit du beliebig viele Ausdrücke miteinander verknüpfen und überprüfen kannst. Control 1 struktwrtn jn
Die Operatoren sind schnell erklärt. Werden mehrere Ausdrücke mit dem logischen UND-Operator verknüpft, wird nur dann true zurückgegeben, wenn alle einzelnen verknüpften Ausdrücke true sind. Sobald einer der verknüpften Ausdrücke nicht mehr true zurückgibt, ist der komplette Ausdruck f alse. Vereinfacht: Bei einer logischen UND-Verknüpfung von bspw, zwei Bedingungen wird nur dann true zurückgcgcbcn, wenn beide Bedingungen true sind. Verknüpfst du hingegen mehrere Ausdrücke mit dem ODER-Operator. dann ist der Gesamtausdnick nicht so anspruchsvoll und liefert bereits true zurück, wenn mindestens eine der verknüpften Bedingungen true zurückgegeben hat, Im Gegensatz zur logischen UND-Verknüpfung gibt eine logische ODER-Verknüpfung nur dann fälse zurück, wenn alle damit verknüpften Bedingungen false zurückgegeben haben. Als Code dürfte dir das Ganze etwas logischer erscheinen // Pseudocode einer UND-Verknüpfung if( (dies«true) && (das==true) ) < II Hier geht es nur rein, wenn dies UND das zutrifft > /f Pseudocode einer ODER-Verknüpfung if( (dies«truc) || (das--truc) ) < II Hier rein geht es, wenn dies ODER das zutrifft > 122
Auf der folgender» Tafel findest du eine Übersicht solcher logischen ODER- bzw. UND-Verknüpfungen: Also ist der NICHT-Operator praktisch ein Operator, den du nicht unbedingt verwenden musst. Zu dieser NICHT-Verknüpfung auch noch schnell der Überblick: IZ""H Natürlich kannst du jederzeit mehr als nur zwei Ausdrücke mit && und 11 miteinander kombinieren. Allerdings kann dies zu einem erheblich komplexe- ren Code führen. Also nicht übertreiben damit! Zum Schluss hättest du noch den NICHT-Operator (I), mit dem ein Aus- druck negiert wird. Du verdrehst damit quasi die Wahrheit. Aus true wird false und aus false eben true Diesen Operator kannst du verwenden, um eine Auswertung abzukürzen. Statt if( dies == false ) kannst du mit dein NICHT-Operator statt- dessen schreiben If (1 dies) iSdiwirrigf Schreib hierzu ein Programm, welches das Volumen eines Schuhkartons berechnet. Das kennst du ja schon. Achte allerdings diesmal darauf, die Werte der Höhe von 10 cm bis 13 cm, die Werte der Breite von 17 cm bis 23 cm und die Werte der Lange von 21 cm bis 29 cm nicht zu unter- bzw. überschreiten Verwende hierfür die logischen Opera- toren. Sollte der Bereich über- bzw. unterschritten werden, setz den Wert einfach auf den Minimalwert. lStrukturvn in <• 123
Hierzu eine Musterlösung der Aufgabe: llnclude <iostrea«i> using namespace scd; int main() < double laenge, breite, hoehe; cout « "Länge (an) : ein » laenge; cout « "Breite (an) : ein » breite; cout « "Höhe (cm) : ein » hoehe; H Wertebercichc überprüfen if( laenge < 21.0 || laenge > 29.0 ) { cout « "Fehler bei der Längenangabeln"; laenge - 21.0; > if( breite < 17.0 || breite > 23.0 ) < cout « "Fehler bei der Breitenangabe In"; breite 17.0; > if( hoehe < 10.0 || hoehe > 13.0 ) ( cout « "Fehler bei der Höhenangabe in"; hoehe 10.0; > double Volumen laenge*breite*hoehe; cout « "Volumen: " « Volumen/1000 « return 0; > Tipp: Für den Fall, dass du auch die Einga- be von ein auf den richtigen Datentyp überprüfen willst, kannst du Folgendes verwenden: if(1 ein » wert) { // Fehler bei Eingabe Nach all den Strapazen haben wir uns, glaube ich, jetzt eine Pizza verdient und strecken mal alle viere von uns. Liter\n"; 124 c«P;t«t fqmi1
Bei zu vielen Verzweigungen mit if. neigt man gerne zum „Falschfahren- und „Über* sicht verlieren**. Wenn du also Fälle vor dir liegen hast« bei denen du mehrere Ausdrücke vom Typ int (oder einem in ein int konvertierbaren Typ. wie bspw, char oder longj auswerten musst, kannst du es einmal mit der Fallunterscheidung switch versuchen. {Achtung] Gleitkommazahlen sind mit der switch-Fallunterscheidung nicht erlaubt und werden vom Compiler mit einer Fehler- meldung verweigert. Der Fall von switch ist dabei ganz einfach: Mit switch überprüfst du den Ausdruck und mit den folgenden case Marken, ob einer der konstanten ganzzahligen Werte dem Ausdruck ent- spricht. Wurde eine Obereinstimmung gefunden, werden die Anwei- sungen hinter der case Marke ausgeführt. Diese case Marken werden natürlich in einem Anweisungsblock zusammengefasst. Trifft keine der case Marken zu, fährt das Programm hinter dem Anweisungsblock fort, Wenn gar nichts passt, kannst du optional eine default-Marke verwenden, die aufjeden Fall ausgeführt wird. Hier dein astrologischer Programmablauf plan dazu: Heiraten switch, jjlt [Schi6dift<er fchimpfti) dzase Frrujer wßg fase rfefäuft Risiko Central 1sCrukturvn in 125
Bleib entspannt, ich habe diese Himmelsiellungen absichtlich so gezeichnet, weil ich wusste, dass du dich aufregen und kalte Füße bekommen wirst. Hier die Antwort auf deine Frage, wie eine theoretische Niederschrift einer solchen Fallunterscheidung aussehen könnte: *1 Hier wird dec Ausdruck ausgewertet der einen ganzzahligen Wert njfuckjpb! (ein int oder einen Wen. der nach int konvertiert werden kann). ’2 Das zurückgegebene Ergebnis wird mithilfe der ca sc-Konstanten verglichen. Finder sich dort eine Übereinstimmung werden die entsprechenden Anweisungen hinter dem Doppelpunkt ausgeführt. switch (Ausdruck) "1 ( case Konstante 1: Anweisungen!; break; *2 case KonstanteZ: Anweisungen?; break; '2 case Konstarite3; Anweisungen3; break; *2 case Konstanten: Anweisungen^; break; "2 default: Anweisungen; *3 } “3 Wurde oc» den vorherigen CaSC-Konstanten kein«? Überein- stimmung mit dem switch- Ausdrwk gefunden, wird auf jeden Fall die optionale default-Marke ingeiprungen. und die entsprechenden Anweltungen dahinter werden Mrlgdiihrt. Die break-Anwcisung am Ende einer jeden case-Marku ist ziemlich wichtig. Mithilfe von break gibst du die Anweisung, dass die Programmausführung aus dem switch-Anweisungs- block herausspringen und dahinter fortfahren soll. Vergisst du eine solche break-Anweisung, dann werden die Anweisungen aller nachfolgenden caae-Marken ausgeführt. Egal, ob diese jetzt zum switch-Ausdruck passen oder nicht. Rein syntaktisch ist di« auch gar kein Fehler, weil es gegebenenfalls ja durchaus gewollt sein kann, dass du alle Anweisungen ab einer bestimm- ten case-Marke ausfuhren willst 126 ctpifi FW
Den Fall bearbeiten Hast du Hunger? Dann habe ich wieder ein Programm für dich. Schmeiß deinen Rechner an und tipp das folgende Listing in deinen Editor. finclude <iostreaa> using namespace atd; int main() ( unsigned inc auswähl; cout « "Such dir was aus:\n"j cout « "-1- Schnittei mit Pommes^n"; cout « "-2- Pizza\n*'; *1 Nach der Auflistung des kannst du dir mit 1. 2 oder J etwas. au$Suchcn und über den EingabcsVeam ein in die Var« ib*c Auswahl «hieben. cout « "-3- Döner Kebab\n'r; cout « "Ihre Auswahl: ein » Auswahl; -1 switch(auswah1) *2 *2 Hier findest du deine Auswahl im SWitch Ausdruck zur Aufwertung Nieder { case 1 : cout « "ca< 900 Kaloricnln"; *3 break; case 2 : cout « "ca, 870 KaloricnVn"; ’3 break; case 3 : cout « "ca. 620 KalorienVn"'; ’S break; default: cout « " 1, 2 oder 3 (Plopp)\n11 j *4 return 0; > ’3 Je nachdem, ob du 1, 2 oder 3 bei der Menüauswahl gewählt haft, w>rd die entsprechende caseAUrke angeiprungen und der Enc*|pcgehjJt des Essens aus- gegeben. *4 Han du bei der Eingabeaufforderung für die Variable auswahl nicht 1.2 oder 3 eingege- ben, finde* switch keinen passenden Fall und m wird die default-AUdce mit einer entsprechenden Ausgabe ausgefuhrt (MetlzJ In der Praxis ist es immer sinnvoll, eine de fault-Marke zu verwenden. und sich nicht darauf zu verlassen, dass der Benutzer deines Programms schon wissen I wird, was er eingibt. ControllStrubturen in 127
Das Programm bei der Ausführung - b««A 4*-l$ Dieter Baer $ ./kaloricnzaehler Such dir was aus: -1- Schnitzel mit Pommes -2- Pizza -3- Döner Kebab Ihre Auswahl: 1 ca- 900 Kalorien Dieter Baer $ ./kalorienzaehler Such dir was aus: -1- Schnitzel mit Ptxwnes -2- Pizza -3- Doner Kebab Ihre Auswahl: 5 1, 2 oder 3 <Plopp) Dieter Baer $ lEtrtliche Aulgibc] Wie würde die Ausgabe des Programms aussehen, wenn die break- Anweisungen fehlen würden? lAntweil] Ab der case'Marke, die mit dem switch-Ausdruck Qberelnstimmt, würden alle übrigen Anweisungen hinter den case Marken und gegebe- nenfalls auch der default-Marke bis 2uni Ende des switch Blocks (oder der nächsten break Anweisung) aus- gefiihrt werden. Egal, ob die case- Marken dann passen oder nicht. Das Programm bei der Ausführung, ohne die break-Anweisungen TeetYMftU b*th 44x10 Dieter Baer $ ./kalorienzaehler Such dir was aus: -1- Schnitzel mit Pommes -2- Pizza -3- Doner Kebab Ihre Auswahl: 2 ca« 870 Kalorien ca. 620 Kalorien 1, 2 oder 3 (Plopp) Dieter Baer $ (Ffhkf/MülIJ Wie bereits erwähnt, ist es kein Fehler, wenn du eine break-An Weisung weg- lässt und damit praktisch zwei Fälle behandelst. Wenn allerdings nicht auf den ersten Blick klar ist. warum du ein break weglässt, solltest du die Code- steile kommentieren 128 rw
Den Fall analysieren int mainO **»£*S<Z*i 44^5 Schön, dass du deinem inneren Schweine- hund sagst, wo cs langgcht. Ich schließe dann schon mal Wetten darauf ab, wie weit du kommst. Bevor du allerdings losrcnnsl. wollen wir den Abschnitt nochmal resümieren. JEinf kehr Aul gab*] Ein Freund vom Römischen Museum hat mich gefragt. ob ich ihm ein kleines Programm schreiben kann, mit dem er Millimelcrmaße in römische Längenmaße umrcchncn kann. Hattest du Lust auf diesen kleinen Job? Die Angaben für die Maßeinheiten kannst du der Tabelle entnehmen. Für die Lösung würde sich switch anbieten. Römische Längenmaße C&oC I Römisch Germanisch ] Länge (mm) I Digital Fingerbreit 18.522 Palmus Handbreit 74.088 Pes FuB 296.352 Cubitu» Elle 444.523 Hierzu meine Musterlösung (oder: P/urifflrtf tfuae codem viae ducunt; aber: Mitte viae duatnt hominem per saecuta Romain (viel Spaß beim googeln oder hingen)): llnclude clostreac» using namespace std; double mm; cout « "Länge (nun): ein » mm; M 4 unsigned int optio; cout « "Ave Caesar, was darf cs scinHn''; ControllStruktur in In <• 129
cout « "-I- Digitus (Fingerbreit)\n"; cout « "-II- Pa1mus (Handbreit)\n"; cout « Pes (Fuß)\n"; cout « "-1V- Cubitus <Ellc)\n"; cout « "Delne Wahl: ein » । optio; switch(opcio) ( case I : cout « mm/18*522 « " digitus^1*; break; case 2 : cout « mm/74.088 « " palmus\n”; break; case 3 : cout « mm/296.352 « " pes\n”; break; case 4 : cout « mm/444.528 « " cubitus\n"; break; default: cout « "Errare huaanum est\n”; } rcturn 0; ) Das Programm bei der Ausführung An«, Ego sum, qui sum S./longitudo Länge (m) : lööö Ave Caesar, was darf es sein? Digitus (Fingerbreit) Palmus (Handbreit) Pes (Fuß) Cubitus (Elle) Wahl: 3 Die Aufgabe hast du prima gelöst! Hätte ich dir gar nicht zugetraut. Da wird sich mein Freund vom Römischen F'W- — Museum sehr freuen. IZrttHl Etwas habe ich noch vergessen zu erwähnen: Ein kleines if-else-Konstrukt könntest du auch mit dem Geiz-ist-g...- Operator ? : erstellen. Zum Beispiel konntest du statt int max, wl, w2 if(wl>w2) { max=w1; } eise { max=w2; } -I- -II- -III- -IV- Deine 3.37437 pes Ego sum, qui sum Folgendes verwenden: max=(wl>w2)?wl:w2; Bei umfangreicheren Bedingungen solltest du aber der besseren Lesbarkeit und dem besseren Verständnis zuliebe [adfrhnurig.’LäivnjJ Zur Belohnung hast du eine kostenlose Jahreskarte für das Römische Museum erhalten. So, jetzt kannst du dich deiner Fitness widmen. Übernimm dich aber nicht! Du hast noch viel vor dir! nicht so geizig mit deinem Code sein, und es einfach beim klassischen if-eIse-Konstrukt belassen. 130 tt»iui roxr
Alles geht, alles kommt zurück, ewig rollt das Rad des Seins. Alles stirbt, alles blüht wieder auf. ewig läuft das Jahr des Seins. Alles bricht, alles wird neu gefügt, ewig baut... Es gibt in der Praxis aber durch- aus Schleifen, die nicht zum Ende finden. Server-Programme, die oft dauerhaft laufen müssen, werden in einer Endlosschleife ausgeführt, die in der Praxis höchstens durch einen Fehler abgebrochen werden können. Aber dies nur am Rande ... Für die Praxis stehen dir drei sol- cher Schleifenkonstruktionen zur Verfügung: Das ist dein nächstes Thema, die Wieder- holung von Dingen. Um wieder zurück zur IT-Philosophie zu kommen, ist hier die Rede von den Iterationen. Bei der Iterativen Programmierung kommen sogenannte Schleifen zum Einsatz, um bestimmte Anweisungen oder ganze Anweisungsblöcke zu wiederholen, Im Gegensatz zu Nietzsches Gedanken sollte die ewige Wiederkunft des Gleichen in Form einer Schleife aber auch mal zu einem Ende finden. Ein solches Ende wird durch eine Abbruchbedingung formuliert. while Schleife: Eine kopfgesteuerte Schleife, deren Abbnichhedingtmg gleich am Anfang erfolgt (daher auch kopfgesteuert). w f Ot Schleife: Auch eine kopfgesteuerte Schleife wie while, nur dass diese Schleife neben der Abbruchbedingung etwas mehr im Kopf hat. do while-Schleife: Diese Schleife ist fuß- gesteuert. Das heißt, dass hier erst einmal alle Anweisungen innerhalb der Schleife ausgeführt werden, und am Ende der Schleife wird die Abbruchbedingung überprüft. Wm d.e eme nicht im Kopf hat. da* hat M-rt den Beinen. K-Ofitrol l Struktur «n jn 1S1
Fangen wir mit der while- Schleife an while( Ausdruck ) *1 < II Anweisungen abarbeiten *2 > II Außerhalb der while-Schleife *3 '1 Hier wird dein Ausdruck tut wahr {true] oder unwahr if alsei h.n geprüft. Die nächste Schleife soll for sein '2 Wenn der Ausdruck zuvor true war werden die Anweisungen innerhalb des while AnwMurtgsblöcl« ausgeführt *3 Ist der Ausdruck vonwhilc hingegen gelogen {false). wird der Anweisungsbtock nächt (mehr) auigeführt, und das Pro- gramm fahrt hinter dem Anweisungsblock fort. for( Anweisung!; Ausdruck; Anweisung2 ) < II Anweisungen abarbeiten } II Außerhalb der for-Schleife Im Gegensatz zur while Schleife, hat die for-Schleife im wahrsten Sinne mehr im Kopf. Hier stehen dir drei Ausdrücke zur Verfügung, die allesamt mit einem Semikolon getrennt werden müssen (auch wenn du so eine leere for (; ;) - Schleife verwendest). Die for Schleife wird in der folgenden Reihenfolge ausgefuhrt: 1 - In der Regel wird der erste Ausdruck lAnwelSungl) (Xar die Initialisierung der Schleifenvariablen verwendet. Dieser erste Ausdruck wird allerdings nur ein einziges Mal ausgewertet Daher kannst du hier auch gleich eine Variable deklarieren. Beachte dann allerdings, dass im Fall einer Deklaration diese Variable nur noch innerhalb des Anweisungsblocks der for Schleife gültig ist. 2 . Der zweite Ausdruck der for Schleife ist in der Regel der Boolesche Aus- druck. mit dem die Schleifenabbruchbedingung überprüft wird. Die Schleife wird abgebrochen, wenn dieser Ausdruck false ist. Gibt dieser Ausdruck true zurück, werden die Anweisungen im Schleifenblock von for ausgelührl (siche 3.). 3 - Jetzt werden die Anweisungen im Anweisungsblock zwischen den geschweiften Klammern ausgefuhrt. fl. Mit dem dritten Ausdruck (Anweisung2) der f or-Schlcife wird in der Praxis meistens die Schleifenvariable erhöht oder reduziert, welche im zweiten Booleschen Ausdruck zur Aus wenn ng verwendet werden kann. 5. Es geht wieder weiter mit 2. 132 Capital FQüf
Und zu guter Letzt do-while fff do { M r ‘ ’ II Anweisungen abarbeiten *2 >while(Ausdruck); ’3 *1 Or Schififenblock der fußgesteuerten do while-Schleife wird mit dem Sthlütselwort do angetanem. ’2 Bei dieicf Schleife werden auf jeden Fall mindestens einmal alle Anweisungen dec Schleifenrumpfes ausgrfuhrt *3 Am Ende dM Schletfetiblocki wird ent der Ausdruck auf eine bestimmte Bedingung hin überprüft Ist diese Bedingung war (true), frnde! rin erneuter Schfeifendurchlauf ab dem Schluwrlwon do *.1.111 Trifft die Be- dmgung n<tit mehr zu (false). wrd d«e Schleife beende! und hinter dem Schleifenrumpf fortgefahren. Pass auf. dass du am Ende einer do-while-Schleife das Semikolon hinter while nicht vergisst’ 1H«ntergrundmfe| Oie do while-Schleife ist optimal, wenn du bestimmte Anweisungen mindestens einmal ausführen musst. In der Praxis eignet sich diese Schleife daher sehr gut für Benutzermenüs in der Konsole. Zum besseren Verständnis nochmals alle drei Schleifen auf die Tafel gezeichnet: f in et wat Anderer PrQgrjrnnubljufpljn der drfi la €*> frfriKiftdtnen Schleifen Controllltruhturin jn < 133
Immer diese Wiederholungen . Auf zum nächsten Schleifendurchlauf. bei dem du das Ganze in der Praxis testen darfst. Wie im echten Leben musst du aber auch hier aufpassen, dass die Gedanken nicht immer um dasselbe krei- sen - sonst begibst du dich in eine nicht mehr endende Schleife. Ein Schritt vor oder einer zurück... Bevor du die Schleifen verwenden kannst, solltest du noch den Inkrement- und den Dekrementoperator kennen. Häufig wirst du nämlich gerade bei einem erneuten Schleifcndurchgang einen Wert um den Wert 1 erhöhen oder verringern wollen. Hier die beiden Zähloperatoren dazu Operator Name Alternative 1 Alternative 2 Inkrement val+=l; val=val+l; — Dekrement val-=l; val=val-1; < INotizI 4 Die Operatoren kannst du sowohl für Ganzzahlen als auch für Kommazahlen verwenden. Bei den Kommazahlen wird Der Unterschied der beiden Möglichkeiten ist natürlich nur der Wert vor dem Komma erhöht oder verringert. Der Operator wird einseitig verwendet, sprich, er enthält nur einen Operanden. Dafür kannst du den Operator links oder rechts vom Operanden stellen. Inkrement Dekrement | Wozu ist es gut? var++ wr— Postfix-Schreibweise: Damit wird Vat um 1 erhöht bzw. verringert, aber der aktuelle Ausdruck bleibt no<h unverändert. +*var —VÄT Präfix-Schreibweise: Damit wird VH V um 1 erhöht bzw. verringert und sofort auf den aktuellen Ausdruck in gewendet. 134 Foxf
Der folgende Codeausschnitt zeigt dir die Implementierung der while Schleife rein programmtechnisch, die nichts ande- res macht, als von 1 bis 5 zu zählen und diese Zahlen auf dem Bildschirm auszugeben: rM Viel leicht probierst du es einfach erst ein- mal selbst aus. anstatt immer sofort in die Well zu fragen oder über mich zu meckern» dann sollte es deutlicher sein. Zum Beispiel: int i - After a while 1; *3 B*’i der Prähoc- SchrcibwciM1 wird der Wert sofort .in dem aktuellen Ausdruck erhöht, was die Ausgabe hier auch beweist. ’1 Hier wird noch 1 ausgegeben, aber hinter dem aktuellen Ausdruck :nacb dem Semikolon) betragt der Wert bereits 2. int zach1er - 1; int abbruch 5; ‘ whilef zaehler <= ( cout « zaehler zaehler++; *4 endlt *1 Hier wird die Schleifenvariable für d*c Abbruch- bediflgung initialisiert. abbruch ) *2 •4 Oer Wen der Schleifenvariablen wud um 1 erhöht (inkrementiert) und dünn mil while erneut überprüft. kJ *3 In dec Schiene wird de# Wert von zaehler über cout ausgegeberi (insgesamt fünfmal). 2 Dir- Abbruchbedingung gibt wahr zurück solange zaehler kleine# oder gleich abbruch Ist. [Notil) Die Präfixschreibweise ++1 ist manches- mal schneller als i++. Bei einem Typen wie int ist dies nicht so von Bedeutung, aber bei Klassen-Typen wie zum Beispiel Iteratoren (lernst du später noch kennen) ist ++1 erheblich effizienter, weil hierbei keine Kopie vom this-Objekt gemacht werden muss. Im Augenblick noch zu kompliziert!? Ok, merk dir zunächst ein- fach in C++ die Präfixschreibweise zu präferieren. C-ontroL (Strukturen jn < 135
Now for Itl Dasselbe Beispiel, welches du eben für while verwendet hast, soll jetzt auch in der for Schleife verbaut werden. Hier das gleichwertige Gegenstück dazu: 1 Hie* wird die Vjiatfe abbruch aif Abbruchbedin- jjutir ünlUalisiert. *2 Im erden Aufdruck deklarieren und Inibaltsieien ^ii die 2Jhlv.vi.ibk* zaehlet Da allerdings diese Variable im Gegensatz zu abbruch Innerhalb de* for-Scblc^c deklamiert wurde, kannst du diese nur noch innerhalb -an for und dessen Anweisungsblocks verwenden AuBerb ilb von f or ßt dieve Variable ungültig ‘5 Nach dein Ausfuhren de< Anweisungen wird die Zählvan-ible zachlcr um den Wert 1 inkrementiert und dann wird wieder die Abbruchbedingung der Schleife überprüft, ab zaehler<=abbruch w.itir ot oder ni-rht, und eben riMsprechend fartgefahren. int abbruch - 5 fortint zaehler“!; Ä2 zaehler<“abbruchj++7.aehler-*5) { cout « zaehler « endl; *4 (Code bcubciten) Im Beispiel der f or-Schleife hattest du auch die Deklaration der Variablen abbruch innerhalb der for-Schleife beim ersten Ausdruck einbauen können. Hierbei musst du dann allerdings die beiden Deklarationen mit einem Komma voneinander trennen. Zum Beispiel: forfint zaehler=l, abbruch=5; ... ; ..») Das Trennen von mehreren Anweisungen funktioniert übrigens auch mit dem letzten Ausdruck der for-Schleife. Du hast sicherlich bemerkt, dass die for-Schleife sehr flexibel ist. Im Grunde kannst du sogar bei allen drei Anweisungen innerhalb der for-Schleife reinschreiben, was du willst, solange es sich um einen syntaktisch korrekten Ausdruck handelt. Du musst nicht einmal alle drei Ausdrücke der for‘Schleife verwenden. Mindestens vorhanden sein müssen aber alle beiden Semikolons: < y» z 7 fort; ;f O, Theoretisch schon, aber du solltest dabei schon noch im Auge haben, dich nicht uni den besten Spagetti-Code zu bewerben. Auch wenn cs nicht for-üblich ist und hier keine Schule machen soll, könntest du das ganze Beispiel folgendermaßen verpacken: fort int zaehlet»1, abbruch-5; II Deklaration zaehler<=abbruch; II Abbruchbedingung cout « zaehler++ « endl II ); 136 Foar
“3 Im nieteten Schritt wird dw Schleifenabbruch- bedingung überprüft, ob zaehler Fußnoten nicht vergessen! Nein, lieber Kari Theodor, die Überschritt ist nur rein kleiner oder gleich abbruch ijt. Tritfl rtir\ zu <f alse) And hinter dem Schlcdcnblock iortgcUhren Ht der Test h .ngegen wahr (t tue) gerne für eine Zähl im Menü hätte. zufällig nirgendwo abgcsch rieben. Außerdem geht es hier nicht um Lynchjustiz, wie sie diverse Gossip-Blii»er oder -Seilen betreiben, bis sich die Fußnägel biegen. Wo wir aber schon bei den Füßen sind, hier soll ein Codebeispiel zur fußgesteuerten do while Schleife demonstriert werden: Int eingabe; = cout « "-1- Circle of Life\n”i *2 1 cout « ”-2- Endless Summer\n"; *2 I cout « *-3- Boring Things\n"; *2 [ cout « ”-99- Exit only\n"; '2 cout « "Your choicc: *3 ein » eingabe; *3 } while ( eingabe I "99); cout « *'Yeah> you‘re out!\n"; f5 Nach oben oder nach unten Innerhalb einer Schleife kannst du mit den Schlüsselwörtern break und continue wie bei einem Aufzug runter- M O»c Eingabe *>fd überprüft Solange diese ungleich 99 war («truci, w»rd die Schleife werter hm jusgdührt. Erst wenn der Wert gleich 99 {-falsei war, wird die Schleife beendet und raus- oder rauf- und reinfahren Mii break fährst du den Aufzug nach unten und beendest eine Schleife sofon. Das Programm fährt hinter dem Anwei- sungsblock fort, wo break ausgcftJhrt wurde, continue hingegen beendet nur die aktuelle Schleifenrunde, fährt mit dem Aufzug nach oben, lässt alle anderen Anweisungen dahinter aus und fängt gleich mit dem nächsten Schleifendurchlaufan. •1 Hier steht der Anfang der dowhile- Sdiielfe. a5 Dies wird nuf j.usgegel>t*n, wenn der Wert 99 ringegrbrn wurde (Aclitunfl Im Gegensatz zu continue kannst du mit break jeden Anweisungsblock beenden, egal ob es sich hierbei um eine Schleife oder eine Verzwergung handelt, continue hingegen ist ein Befehl, den du nur in einer Schleife verwenden kannst. Control 1 Strukturen In C 137
Und alles noch einmal Jetzt darfst du die Iterationen noch einmal in deinen Gedanken kreisen lassen. Ist dein Geist milde und verweigert sich, empfehle ich dir einen grünen Tee (Sorte Gun* powder). der dich wieder beleben wird. lEtetahe Aiiigibe] Das folgende Listing ist absolut kein „Leonardo Da Vinci“ dem eher ein .Paradox der Hässlichkeit“ und lauft bei der Ausführung außerdem in eine Endlosschleife. Beschreib bitte, was der Code zu machen versucht und finde vorher den Fehler? Hier der Quälcodöi finclude <iostream> using namcspacc std; int main() int ( gibrairzwei 138 Uc'.t«! Fon? mirzwei - le(gibmirr>rci if( gibmtrzwei contlnue; *1 20 ) 2 2 )
end { cout « "Fertig! \n’'; break; } cout « giboirzwei « ++gibmirzwei; > return 0; > bei ein gib- DER FEHLER liegt, glaube ich, continue *1. Sobald mirzweiZ2 einen Rest zurückgibt, ist die Bedingung true, und continue wird ausführt. Da allerdings gibmirzwei nicht mehr erhöht wird, findet das Programm nicht mehr aus diesem klei- nen Bereich heraus, und es wird immer wieder ENDLOS von continue zur Schleife und von der Schleife zu continue durchlaufen, weil die Schleifenvariable gibmirzwei nicht mehr verändert wird. Folglich muss eine Zeile vor continue ein ++gibmirzwei; eingefügt werden. Was das Programm macht, habe ich zwar zu- nächst auch nicht kapiert, aber dann ausprobiert! Es werden sämtliche GERADEN ZAHLEN von 0 bis 18 AUSGEGEBEN! Respekt, Schrödinger? Hätte ich dir gar nicht zugetraut. Okay, dann noch eine etwas schwierigere Aufgabe: |$thwi« ri gr Au[g.ibe| Schreib das Programm von eben um. Verwende dafür eine for-Schleife und verzichte bei der Ausführung komplett auf continue und break Control 1 Struktur«n in <*• 139
linclude <i.ostreain> using namespace std; int TnainO < for(int gibmirzwei “ 0; gibmirzwei < 20; ++gibmirzwei) { if( I (gibmirzwei 22} ) { cout « gibmirzwei « endl; } cout « HFertig\nM; return 0; Prima, das ist fast perfekt Theoretisch könntest du in diesem Beispiel außerdem auch noch statt ++gibmirzwei eben gibmir zwei+=2 verwenden und könntest so auf die if-Überprüfung komplett verzichten. |Noti«rrn?Üton| Ein häufiger Fehler bei Schleifen ist eine falsche Abbruch- bedingung, die entsteht, weil bei einem Schleifendurchlauf eine Schleifenvariable, die für den Abbruch wichtig ist, falsch oder gar nicht (re-)initialisiert wurde. Häufig kommt es dann zu einer Endlosschleife, die du nur mit Gewalt von außen beenden kannst. |Belüh*iunf/L6tütftg| Prima, du hast die Abbruch Bedingung des Kapitels erreicht und kannst somit die Seiten inkrementieren. Vorher solltest du aber im wahrsten Sinne des Wortes ein .break" einlegen. 140 c«o;t«i roxr
_ __ _ _ _ _ Arrays, Strings, ______0 E ft Lj O Vektoren, ^^^B ^B Strukturen und Zeiger Vonngleichen und unter- schiedlichen Typen, dem Sternchen und anderen ungemütlichen Sachen Schrödinger kann es drehen und wenden, wie er will. Mit einfachen Typen kommt er In C++nicht weit. Er ahnt schon, dass ein ungemütliches Kapitel folgt. Hierbei lernt er gleich, dass er eine ganze Reihe von Typen In einem Array unterbringen kann. Unterschiedliche Typen hingegen kann er In eine Struktur otecken. Schrödinger lernt In diesem Kapitel die Bedeutung des Sternchens (*) bei den Variablen kennen. Das Kapital Ist sehr verwirrend für Schrödinger, well es hier Zeiger gibt, die wild sind; um eich davor zu schützen, kann man auch einen Nullzeiger verwenden. Schrädinger findet das komplette Kapitel recht wild und hat Im Augenblick null Ahnung, wofür Mmas gut sein soll. Schün, Zeiger können auf Adressen verweisen, als Funktionsparameter und Funktionsrückgabewert oder für noch komplexere Dinge verwendet werden, aber bei den ersten Erklärungen sieht Schrödinger eher ein schwarzes Loch statt leuchtender Sternchen. Aber er lot sich sicher, dase er nach diesem Kapitol erleuchtet wird.
Sicherlich hast du dir schon deine Gedanken gemacht, was du tun willst, wenn du von einem Datentyp mehrere Werte für eine ähnliche Aufgabe brauchtest. Wie würdest du bspw. Vorgehen, um die monatliche Erzeugung (Januar bis Dezember) von Schuhkartons zu protokollieren? SC? ? unsigned int januar, februar, ... Nein, nach deinem bisherigen Wissen ist das zwar okay, aber in der Praxis doch sehr umständlich. C++ bietet dir hierfür die Möglichkeit, gleiche Typen über ein Array (auch als Vektor oder Reihungen bekannt) zusammen zu setzen. Das Aneinanderreihen von Typen über Arrays ist übrigens dann auch die einzige Möglichkeit, Zeichenkellen (auch Strings genannt) in C++ zu verwenden. Eine solche Zeichenkette wird natürlich aus einer Reihe von char-Typen gebildet. In der Praxis stehen dir zwei Möglichkeiten zur Verfügung, solche Arrays in deinem Programm zu verwenden. Da wäre zum einen der manuelle Weg über C-Arrays bzw, C-Strings, was allerdings eher für Masochisten geeignet ist. die gerne mit einem allen Oldtimerohne Schnickschnack fahren wollen. Auf der anderen Seite sind da die Komfonklassen vector und String für Leute, die gerne Autos mit viiiieeeel Komfort und der neuesten Technik fahren wollen. In diesem Abschnitt lernst du erst einmal den aaalten Dinosaurier-Teil, die ungespritzten Bio Arrays, kennen. 142
Es war einmal, vor langer Zeil, ein einzelner Typ namens Ze, der wollte nicht mehr alleine sein. Er war so traurig, dass ihm der Ze-Großvater einen Indexoperator [ ) schenkte. Von nun an wurde Ze nur noch das Ze-Array genannt. Der Ze-Großvater hat festgelegu dass ein Ze-Array (C-Array) immer wie folgt auszusehen hatte: ZeTyp Arraynaobe (Anzahl_Elements |; Der Großvater beschloss, dass der ZcTyp von beliebigem Datentyp sein konnte, und als Arrayname konnten alle erlaubten Bezeichner verwendet werden. Wie viele ZeTyp ’ en tn Arrayname gespeichert werden konnten, legte man dann mit Anzahl_ELemente zwischen den eckigen Klammern fest, was natürlich ein ganzzahliger Ausdruck größer als 0 sein musste. Zum Beispiel: unsigncd int zcarraylS]; zearry konnte hier fünf Elemente vom Typ uns igned int abspeichern. Der Wert der einzelnen Elemente wurde nicht automatisch initialisiert und war unbe- kannt. Die einzelnen Ze* Elemente im Ze Array konnten so über Arrayname und den Indexwert zwischen den eckigen Klammern angesprochen werden. Das Groß Väterchen sagte auch, dass das erste Element im Ze-Array mit dem Index 0 anfangen musste und somit das letzte Element immer Anzahl^Elemente-1 lauten musste. Mit Werten ernähren konnte man ein Ze-Array ganz einfach •1 Hier wurde das Ze-Array bereits bei der Initialisierung mit einer Liste von durch Kommas getrennten Werten zwischen weiften Klammern gefüttert. unsigned int zearray_l(3] - { 1, 2, 3 }; *1 zearray_l[O) 7; ’2 zearray_l(2) 9|*^ ein » zearray l [ 1 ]; -1 unsigncd int zcarray_2|] { 555> 777 }; ’4 unsigned int zearray_3(100J { 111 >; *5 *2 Dai Ze-Array kann auch jrdrfrrit nach der Fütterung Immer wieder mit neuen Werten mithilfe des Indexoperators und des entsprechenden Index wertes gefüttert weiden Aber Achtung, dai erste Element beginnt immer abO! ’S Bei einer größeren Uängcnangabc .n$ Elemente in der InitiaMierungUiste vorhanden sind, haben die renlkhen Elemente automa- tisch den Werl 0 ‘4 Bei einer Initialisierungsliste d-irt auch di> Llngenangabe fehlen In dem Fall hast du ein Zr-Arr.iy mit zwei Elementen gefüllt. *3 Natürlich funktioniert diele Fütterung mithilfe dei InrfrKoprratoo auch üh<r die Tastatur m4 dem Stream C in. Indem der Wert nut dem E»ngabeoperator» in das entsprechende Array-Efement gewopft wird. Irriy;. Strtngs. Vffctorpn, Strukturen und Z>*9*r 143
Natürlich verträgt sich das Ze-Array auch mit dem Typ char. Hier kann es ganz ein- fach nur als Ze-Array existieren, welches mi! Buchstaben gefüttert wird. Großvatcrchen nannte dieses Ze-Array mit Zeichen jetzt einfach mal Ze-String (C-String). Daniil ein solcher Ze-String nicht einfach als ein Ze-Array mit Zeichen anerkannt wird, hat cs ein ganz besonderes Nullzeichen (\ 0) am Ende erhallen. Dieses Zei- \ ' “* * chen schließt einen gültigen Ze-Strlng ab. Allerdings musst du hierbei darauf Xz*ta»<rifw»twtio«»] achten, dass Platz für das String-Endezcichen vorhanden ist, sprich, dein Ein Ze-String (C-String) Ze-String sollte immer um ein Zeichen größer sein, als die Anzahl der sicht- sollte absolut nicht mit der baren Zeichen! großen String-Klasse von Für die Nachbearbeitung von Ze Strings haue Großväterchen einige Funktio- nen aus der Hcaderdatei <CString> zur Verfügung gestellt. Damit konn- ten Ze-Strings z. B. getrennt, zusammengefügl oder verglichen werden. Aller dlngs ist die Verwendung dieser Funktionen doch schon ziemlich sperrig und wurde häufig falsch verwendet. C++ gleichgestellt werden. Im Grunde bleibt ein Ze- String ein einfaches Feld oder eine Reihe von Zeichen. Zeichenkette oder String? (Begriff sdefinltl#n| Beide Begriffe werden in der Infor- matik für dasselbe, für eine Folge von Zei- chen. verwendet. Um allerdings die alten Strings von der Klasse string auseinander- zuhalten, wird hier der Begriff Ze-String (C-String) für die alten Strings verwendet. Ansonsten ist die Verwendung eines Ze-Strings wie schon bei einem Ze-Array recht ähnlich: ‘2 Hier kennen 33 Zekhrn gespeichert werden. Die ersten wr Zeichen sind h irr m 4 1 H 1, rb’. ’c1 und dem Trrminie- rungwekhen 1 \0' worbclcgt D»c restlichen Zeichen werden automatisch mit 0 (genauer 1 \0*) belegt. *1 Natürlich kann auch hier be» der InitQliüerungsliste die lnd«xangabe fehlen ’1 Hiermit kannst du 22 einzelne Zeichen speichern. char sestringel[22]; *1 char zestring_2[33) « { «a't 'b'p ’c', '\0’ ); *2 char zestring_3[] « { 'a"t ’brt ’c’« ’\0' >» *3 char zestring_4[ ] { •'abc" }; ‘4 -4 Die umständliche Schreibweise mit den einzelnen Zeichen kann auch Frischen doppelte Hochkommjif gestellt werden. Der Vorteil an dieser Schicifrweise ist. dass du das Stiinß-Terminierungs- zeicben nicht extra hinzu« fugen mißrt. Bei doppelten Huchkqmmas geschieht dies automatisch ein » zestringl; // BUUUUUUMMMM *5 cin,gctlinc(zcstrlng_l, 20); *6 *5 Das Einlesen eines Zr-Strings über den Stream ein mtihltfe des Emgabeoperatw » üt zwar möglich, aber da hier keine Längenüberprüfung stattflndet. sehr gefährlich Außerdem t>r»ctn ein hierbei das Einlesen ab dem ersten Leer-, Tab- oder Neue-Zeiie- Zekhen ab. Die Methode getlinc () gehöit ebenfalls zum Stream ein und stetlt die bessere Alternative zum Einlesen von ZeStrings dir. Zum einen kann htcr die Lange der Emgabc beschrankt werden, und cs werden auch leer- und TabuUtor- zcichcn cingcfcsen 144 sechs
int main() gesamt+-di,vnk[i]; ’5 *3 Ober den Strcam ein lc5CH wir den Wert von der Tastatur cm und schieben diesen ober den Eingjbeoperjtix #in, <lostream> using namespace std; Gib mir rohen Input (Bio-Arrays) Ich weiß, das Thema Ist nicht ganz rostfrei, aber ich kann cs nicht einfach ignorieren. Arn besten machst du hier noch bei den beiden Listings rnil, und sag dir dann später im Wohnzimmer, dass du ohne die alten Ze-Sachen ein wesentlich schöneres Leben hast. Hierzu ein für dich vielleicht nützliches Programm, welches berechnet, wie viel Liter Flüssigkeit du pro Tag zu dir genommen hast in d i-r entsprechen- de Array-Element mit dem Index 1. welches wir In jedem SctileifendurchUui erhöhen ’ 1 Hier steh! das Ze-Array, In dem deine sechs Trlnk- elnheiten gespeichert werden. ’2 Da es kerne Funktion gabt. welche d»e Länge eines Ze-Arrays etrndlell, müssen wir um hier mithilfe des Sizeof Oper.itars die Anzahl der Elemente selbst ausrechnen Hierbei berechnest du run.ichst die Gesamtgröße des Arrays getclh durch den Speicherbedarf des Datentyps. double drunk[6] « {0.0} fortsize t i-0; Ksizeof(drunk)/sizcof(double); £>♦) *2 cout « "Wie viel Liter getrunken (" « i+1 « ein » drunk[i]; *3 r4 E»n erneuter Schleifen- durchlauf Aller Arrays ... double gesamt 0.0; - J for(size_t i-0; i<slzeof(drunk)/slzeof(double); ‘4 “5 ... bei dem alle einzelnen Array-Elemente zusammenaddiert werden. cout « "Getrunken : " « gesamt « " Liter\n"; return 0; [Achlungi Pass auf. dass du nicht <= statt < bet der for- Schleife verwendest. Mit dem <=-Operator wür- dest du in diesem Beispiel den Puffer überlaufen (Äu//er-Over//ow). Vergiss nicht, bei einem Array wird beim Zählen mit 0 angefangen. Kann man gar nicht oft genug erwähnen. Neben der etwas umständlicheren Verwendung des Ze-Arrays dürfte dir hier gleich ein weiteres Manko auffallen» Wenn sich dein Trinkvrrhaltrn nämlich ändert und dir dir Array-Größe von sechs Einheiten nicht mehr ausreicht. * Nerven dich .Tricks" wie "sizeof (drunk) / sizeof (double) •*. könnte ich dir auch die neue for-Schleife (genauer range-basierte-for- Schleife) anbieten, welche mit C++11 einge- fuhrt wurde. Kombiniert mit dem Schlüsselwort auto, lassen sich Ze-Arrays oder diverse (STL-) Behälter sehr kompakt und bequem durchlau- fen. Bezogen auf die Callouts *4 und *5, verein- fachst du dir das Leben folgendermaßen: forfauto &d : drunk) { gesamt+=d; ) lrr«yf. Strings. V«ktar«n. Struktur»" und Z»>g*r 146
musst du das Programm ändern. Die Größe eines Ze-Arrays lasst sich während der Laufzeit nicht so ohne Weiteres .Indem. Es gibt zwar die Möglichkeit, das Ganze dynamischer zu machen, aber den Aufwand kann man sich sparen, well es dafür schon eine Klasse gibt, die das alles (und noch vieles mehr) fix und fertig anbietet. Natürlich will ich dir hier auch ein sinnfreies Ze-String Beispiel nicht vorenthalt linclude «lostream* linclude «cstring* *1 linclude «locale* *2 using namespace std; ‘2 Diesen Herder brauchet du für landestypisehe Einstellungen und verwen- det die Klasse locale später für toupper( ) •1 Den Hcadcf brjixhst du. um hier die Ze-Funkti- o-n strlcn() und strne- py ( ) verwenden nj können. In der Schleife wurde der primitive Typ size_t (std:: size_t) anstelle von unsigned int verwendet. Der Typ wird gerne als Vergleichs- oder Rückgabetyp verwendet und ist letztendlich wieder nur eine kürzere Bezeichnung (genauer cypcdcf) von unsigncd int. unsigned long oder gar unsigned long long (also: Implementierungs- abhängig). int maln() r V " { "3 Hier stehen j char machgross [25]; '3 J . unsere b«den J char backupme [ 10 ]; *3 Ze-Strmg». £ locale loc; cout <« "Gib was ein: ein.getline(nachgross, 25); cout « "Deine Eingabe: * « machgross « en for(slze_t 1-0; Katrlen(machgross); !♦+) ’6 < cout «< toupper(machgross[1), loc); > cout « endl; ^4 strncpy(backupme, machgross, 9); ‘8 backupme[9) = ’\0‘; '8 । cout «« backupme « endl} return 0} > •4 Der Ze-String machgross kriegt seine Daten vom Stream ein mithilfe de* Methode getline ( ) vpn der Tastatur 7Ußc*choben Ä K /• A ( fc& Den Ze-String machgross 5 Oie Ausgabe wr wollen wir hier Zeichen des Ze-Strings für Zeichen durchlaufen. machgross jt Mtt«trlen() mi rjSl W J dem Header «cstring* J tx-kommit du dk Anzahl der . / Zeichen ohne das Swing- lf Endezeichen tuiück •? Die einzelnen Zeichen werden über toupper ( ) als Großbuchstaben ausgegeben. Kann der Buchstabe von touppet () nie hi umgewandelt werden oder ist er bereits ein Großbuchstabe, wird das Zeichen unverändert zurütkgegeberi. “Ö Danul werden die ersten neun Zeichen w machgross nach buckupmc «ptert. Ist machgross kleiner ab backupme werden die restlichen Zeichen mit dem \0-Zeichen getollt. Kein String-Terminierungs- zeichen w id liinjugetügt. wenn machgrosS mehr dis neun Zeichen enthält und diese njcli backupme kopiert. In dem Fall munt du dich selbst um das String-Endcieichcn kümmern. Ich denke, dieses Beispiel demonstriert dir deutlich, warum die Ze-Sachen keine gute Wahl sind. Die umständlichen Funktionen von <CString> sind da oft keine Hilfe, well man immer auf das \ 0 Zeichen achten muss. Mal gibt eine Funktion einen Wen mit dem und mal wieder ohne das \0-Zeichen zurück. Und mal ehrlich, das Programm ist auch nicht unbedingt lcicht_yerständlich. (Notiri In der Headerdatei «catrlng* findest du natürlich eine ganze Palette weiterer Funktio- nen, um einen Ze-String zu verarbeiten 148 c*»;t«i sechs
Das macht keinen Spaß Auch wenn dir bis jetzt die besseren Alternativen zum Ze-Array bzw. Ze-String noch nicht bekannt sind, dürftest du bereits festgesiellt haben, dass GroKvlterchen mittlerweile ausgedient hat. Bei den Ze-Arrays bzw. Ze-Strings ist sehr viel Handarbeit erforderlich. Es gibt kaum Funktionen dafür, und wenn, Ist deren Anwendung nicht unbedingt einfach. Die Vergangenheit hat zudem gezeigt, dass es eben diese Handarbeit auf dein rohen Speicher war. die zu unzähligen Fehlem wie Pufferüber- bzw. -unterlauf geführt hat. Die No.1 (Göogle findet Ca. 2,5 Millionen Links) der ewigen Hitliste von Fehlern bleibt nach wie vor der Pufferüberlauf (Buffer-Overflow). Amys* Strings* Vektoren* Strukturen und Zeiger 147
VbfrKhrfiUf* dr> 2’dbn*iChft finfl Puffer» hnn üUlf f Gfe<n h*b<n Bin solcher Oberlauf ist schnell erklärt, du hast ein randvolles Glas mit Wasser und schüttest weiterhin Wasser darauf. So Ist es z. B. auch bei den Ze-Arrays bzw. Ze-Strings. Wenn du ein Array mit einem Puffer von fünf Elementen (z.B,: int iarr [ 5 ] oder char carr (5)} hast und schreibst noch ein sechstes Element rein, wird hinter dem Zielbereich des Puffers eine Speicher- steile überschrieben [Achtung tfoi’M Cht| Bei den Ze-Strings kommt neben den Problemen mit der Langen Überprüfung des Ze-Arrays noch das String-Endezei- chen f \ 0 1 dazu. Auch hier wurde in der Vergangenheit viel Schindluder getrieben und gerne mal der Platz für das Zeichen vergessen oder auch hier über den Grenzbereich hinausgeschrieben. Das kann man nicht 100%ig sagen. Zunächst mal kann cs sein, dass gar nichts passiert oder das Programm irgendwann ab- stürzt. Eine weitere Möglichkeit ist. dass mit falschen Werten weitergearbeitet wird. Schlimmer wird es dann, wenn die Um- gebung der Laufzeit beschädigt wird. Dann freuen sich Hacker über die offen gelassene Tür und können allerlei Unfug treiben, Besonders schlimm ist das, wenn dein Programm auch über das Internet läuft. Krankheiten von Ze-Arrays und Ze-Strings Hier nochmals in Stichworten, was für und was gegen die Ze-Varianten spricht «“ Es fehlen Kontrollmechanismen, um Pufferübcrläufe (Buffer-Overflow) zu vermeiden. Auf der anderen Seite kann aber eine Eigenverwaltung des Puffers auch ein Plus an Geschwindigkeit darstcllcn, wenn cs denn darauf ankommt. Ist der Puffer mal voll, muss ein ziemlicher programmtechnischer Aufwand betrie- ben werden, um genügend Speicherplatz zur Verfügung zu stellen. Wird hier schlampig gearbeitet und bspw. ein nicht mehr benötigter Speicherplatz nicht freigegeben, entstehen sogenannte Speicherlecks (Memory Leaks). 148 C«»K*l SECHS
Beim Ze-String muss zudem noch recht umständlich das String-Endezeichen ’ \ 0* extra behandelt und brachtet werden. " Es fehlen im Allgemeinen einfach spezielle Funktionen, um mit den Ze-Arrays zu arbeiten. Alles muss umständlich und manuell erledigt werden, wobei gerne wieder Fehler gemacht werden. Die Ze-Strings haben zwar mir <cstring> eine Bibliothek dafür, aber die Verwendung der Funktionen dort ist ebenfalls wieder recht umständlich, und es wurden auch gerne Fehler damit gemacht. iH^tifrrn/Übcn] Sofern du also keine Klasse auf einer tieferen Ebene erstellen musst, spricht eigentlich nichts für die Ze-Arrays bzw. Ze-Strings. Statt auf Ze-Arrays solltest du daher auf die Klasse vector und statt auf Ze-Strings auf die Klasse sträng zurückgreifen. (EiAfah* Aul^bc] Das Beste kommt zum Schluss. In den folgenden Codezeilen findest du nochmals ein paar klassische Fehler, die gerne mit Ze-Arrays bzw. Ze-String$ praktiziert werden. Kannst du die Fehler entdecken? Deck die Lösung ab int zeArray[5]; for(int 1-0; i<-5; 1++) M < zeArrayll) 1; '1 Das hier Ist ein klassischer Pufferüberlauf Hier wird ein Aifdy mit sechs Werten initialisiert, obwohl nui für fünf Elemente Platz reserviert ist. Der Fehler liegt beim <=-Operator. Du musst nu< < ohne — verwenden. > char zeStringOl() - "Ze ist blöd'*; char zeStringO2[6]; for(int 1-0; i<scrlen(zeStringdi); *2 "2 Djy h er ist ein schlimmerer Pufferiiberlauf Wegen Strien ( zeStringOl ) werden hier alle Zeic hen von zeStringOl wh zeStringOZ kopiert Che Zeichcnkettc "Ze ist blöd” hat aber «hon weitaus mehr .als. sechs Zeichen zcString02[1] - zeStringOl[1); *2 > zeStringO2[61 = 'ÄO'; ’3 *3 Und noch Men kUssiscben Denkfehler und wiede- einen Puffer- überlauf finden wir hier. Hier wird dis siebte Element mrt dem String- Ende beschrieben. Nicht vergessen, cs wird bei 0 mit dem Zahlen angefangen. Arrjyt* StrLnqs» V*k t 0 r g n » Strukturen und Z«>9«r 14»
Die gleichen Typen In einer Reihe entstellen und wieder zurück Nachdem du dich jetzt der Tortur der Ze-Arrays und -Strings ausgesetzt hast, darfst du dich wieder den schöneren Seiten von C++ ztiwenden. In der Praxis verwendest du in C++ (meistens) fertige und lang erprobte Klassen statt selbstgestrickter Sachen. So ist es auch mit den Klassen vector und String, welche in der Regel immer die bessere Alternative zu den Zc-Arrays und Ze-Strings darstellen. IHaftEerfrundinfa) Warum bei den Klassen vector und string vieles so funktioniert, wie es eben funktionieren soll, verdanken diese Klassen speziellen Techniken, wie etwa dem Überladen von Operatoren, Elementfunktionen und vielen weiteren speziellen Klassentechniken, die du am Ende des 8uche$ auch selbst verstehen und implementieren können wirst. Die Komfortklassefn Mit den Klassen vector und String stehen dir eine ng c icrvjCren e/imusst (ZetWl] Die Klassen string und vector haben unglaublich viele Funktionen implemen- tiert, weshalb du hier , nicht um die Referenz deines Compilers oder weitere Recherchen im Web herum- kommst. Menge nützlicher Funktionalitäten zur Verfügung. die du von einem Array IfcsyStri 4«) darfst. Audi unu> das'fiachlräglichl^ Rcsi vorr>eiterem Sntfcfu du di?hler kdrtc'Ufrrf machen. Puffcrübcrlä ebenfalls abgefangen |H»ntrr|;rund‘nta| Da du die Klassen bisher im Buch noch nicht kennengelernt hast, behandelst du vector und string vorerst einfach wie einen gewöhnlichen neuen Datentyp.
Für Arrays empfehlen die 0*-Doktoren die Klasse vector. Um die Klasse verwenden zu können, musst du die gleichnamige Hcadcrdatei <vector> mit einbinden. Alle vector Sachen sind auch hier im Namensbereich Std enthalten. Die Syntax, ein Array inllhllfe der Klasse vector anzulegen, steht zunächst etwas abenteuerlich aus, aber man gewöhnt sich schnell daran: vector<Typ> Arrayname (Anzahl Eleeence); Nach vector musst du zwischen den spitzen Klammern deinen Typ angcbrn, von dem du ein Array erstellen willst. Dann folgt der Atraynante als Bezeichner, und am Ende gibst du zwischen runden Klam* mern die Anzahl der Elemente (AnzahL_Elernente) an, die dein Array vom Datentyp Typ speichern kann. Ein Array mit fünf unsigned int Elementen kannst du mit vector wie folgt definieren: vector<unsigned int> vecray(5); Im Gegensatz zu den biologischen Ze-Arrays werden hierbei auch gleich alle Elemente mit 0 initialisiert. Den Zugriff auf die einzelnen Elemente des Arrays kannst du wieder über die eckigen Klammem [ n) realisieren: vecraylO) = 12345: ein » vecray[I|; cout « vecray[OJ « endl; IZettelJ Seit C++11 ist es endlich auch möglich, die vereinheitlichte Ini- tialisierungssynUx mit der { }-lnitralisierungsliste für vector zu verwenden: vector<unsigned int> vec96{ 12, 34, 56, 78 }; vcctor<unsigned int> vec69 • { 12, 34, 56, 78 }; Hiermit legst du vec96 und vec69 mit jeweils vier uns Igned int-Elementen an. die alle auch gleich mit einem einem Wert belegt werden. Hier bekommt das erste Element im Array den Wert 12345 zugewiesen. Statt des ( ] -Operators kannst du auch die Elementfunktion at ( ) verwenden. Der Vorteil dieser Funktion ist. dass sich damit auch Puffer- überläufe (Buffer-Overflow) abfangen lassen, indem eine Ausnahme beim Überschreiten des Bereiches ausgclösl wird. Zum Beispiel: vecray,at(0) 12345; ein » vecray,at(I); cout « vecray.at(O) « endl; vecray,at(5) 9876; II II! Puf fcriibcrlauf II! *1 M Das Programm wird bei dieser Codezeile mit einer Ausnahme beendet weil versucht wurde, rn einen nicht mehr erlaubten Speicherbereich ju schreiben. 4 Achtung] Beachten musst du natürlich auch hier, dass das erste Element im Array den Index 0 und das letzte Element den Wert n-1 besitzt! Die Anzahl der Elemente zur Laufzeit kannst du bei den vector Arrays mit der Elementfunktion eizeO ermitteln. Zum Beispiel: cout « vecray .$lze() « endl; irrjy^ $ t r j n q y «. Vektortn, St r u k t M*~<-n E»ag»i 1S1
Die l.uxusklassc String kannst (solltest) du als die Alternative zu den Ze-Strings verwenden. Die Klasse bietet alles, was du dir von den Ze-Strings gewünscht, aber nicht bekommen hast. Neben einer Überprüfung auf einen Pufferüberlauf findest du auch hier die Funktionalität, um erneuten Speicher für weitere Zeichen anzufordern. Die Anwen- dung ist denkbar einfach, vielseitig und unglaublich flexibel. Um die Klasse verwenden zu können, musst du den Header <string> in deinem Programm ein fügen: scring einStringOl; string einStringOZ = “Voll einfach"; >3 *1 Em leerer String w>>d "2 Ein String wird gleich beim Erzeugen mit einer Zeichen- kette belegt. •3 MR einer einfachen Zuweisung wwd der String kopiert clnStrlngOL • cinScringOZ; string einStringO3; getllnefcin, einStringO3); *4^ cout « ei.nScringO3 « endl char zearray[j "Ze-Array2String";-^5 string einStringOi - zearray; *5 Natürlich ist die Klasse string auch nichts anderes als eine Zeichenkette, die •4 Das Einlesen von Tastatur über den Stu-am ein wird hier übci getlincf) für eine gante Zeile realisiert. Würdest du hier den Eingabeoperator » verwenden. würde nur b>s rum ersten leerjeicben eingelesen Die Ausgabe über den Stre.im COUt und den Autgabeoper.ilor « funktioniert WC gewohnt, aus Zeichen vom Typ char zusammenge- setzt ist. Als rohe Folge von Zeichen könntest du durchaus auch vector <char> verwenden. Aber da solche Zei- chenketten doch viel spezieller als Arrays sind, und auch andere Funktionen als gewöhnliche Arrays benötigen, wird hier- für eine extra Klasse string angeboten und sollte auch verwendet werden. IZetiel) Natürlich kannst du auch mit string die neue vereinheitlichte Initialisierungssyntax verwenden, die es seit C++11 gibt: string s66{"C++11-Initialsierung“}; string s99 ("Auch C++11“}; 152 scchs
Nie mehr Blo(-Arrays) Naja, ganz so drastisch, wie cs die Oberschrift hier darstcl11, ist es wieder nicht. In jedem Array der Klasse vector oder String der Klasse String steckt schließlich auch ein geringer Bioanteil. Bezogen auf den Sprit F.10 konnte man hier auch schreiben AlOoderSIO. Um einen Vergleich zu den Ze-Arrays zu haben, findest du hier nochmals das Listing zu deinem Trinkvcrhalten in Litern am Tag. Nur diesmal mit der Klasse VCCtor: linclude «iostreacn* linclude <vector> *1 using namespacc std; . D^s ist e-r feeres 1 Ohne drn Header geht es nicht. int rnainO ( vector<double> drunk double now; Array mit anüngllch o Elementen "3 M»thitfe der Elemenrtunktiofl push_back() wird das neu eingegebem.' Element hinten ,im Array hinzu gefugt Um d.<* Speiehcrvcrwiltung rwM du dich dabc» nicht kümmern cout « "Wieviel Liter getrübten (-1 = Ende): ein » now; if(now !- -1) drunk.push_back(now); }while(now ! -I); double gesamt = 0.0; ’4 Mithilfe der ElemerHfunktlon sizc() wird dk Anzahl der Elemente im A/ray zurütlkgegeben. Wesb-ilb wir urw auch hie< nkht groftda«um kümmern müssen, ah in der Schiede d ese Element- Funktion zu verwenden. for(size_t 1*0; { 1 < drunk.size(); !♦*) gesamt+=drunk[i]; -5 “5 Hier addieren wir cout « "Gectunkegesamt << M Llter\n"; Ei. । . r !<• t a»m. rgturn-Or zusammen. Statt drunk(1] hattest du hier INclizJ natürlich auch drunk.at(1) verwenden können Und ja, auch hier funktioniert die neu in 0*11 eingeführte range-based-for-Schleife bestens und genauso wie ich es dir bereits zuvor schon bei den Ze-Arrays gezeigt habe. Bezogen auf die Callouts '4 und *5 kannst du dir auch hier das Durch- laufen der einzelnen Elemente wesentlich einfacher gestalten: fortauto kd : drunk) { gesamt+=d; > Irreys. Strings. Vektoren. Strukturen und Zeiger 163
Ilinfqchr Aufgabe] Welchen Vorteil hatte das Beispiel eben im Gegensatz zum selben Beispiel mit den Ze-Arrays? Genau! Die Vektoren sind absolut dynamisch, und auch die Anwen- dung Ist wesentlich einfacher. so dass unnötige Fehler vermieden werden können. •1 Den Hr.idcr br-Äixhst du. um die Klasse string zu verwenden Natürlich will ich dir hier auch das Gegenstück zum sinnfreien Ze-String-Beispiel, diesmal mithilfe der string-Klasse, präsentieren linclude <iostream> llnclude <string>pi linclude <locale> 2 H*er werden jwri leere Stnng angelegt using namespace std; int mainO < string machgross. "3 Wir lewi die ganze Zelle vorn Eingabestream ein ein. backupme; ’4 Mithilfe de* Elementfunktion Size() bekommst du die Anzahl def Zechen des Strings zurück Alternativ könntest du hierzu auch die Elementfunktion length() verwenden locale loc; cout « "Gib was ein: •'; getllne(cin» caachgross); cout « "Deine Eingabe: " « nxachgross « for(size_t i-0; Kmachgross>size(); 1++) { cout « toupper(mchgross>at(i), loc); cout « endl; zu kopieren, backupme - mach&ross; *6 geht hier ganf cout << backupme << endl; einlach mit dem «CUm 0f Zimeaun^- operator. endl; ’S Wir geben das aktuelle Zeichen mit dem Index 1 als Großbuchstaben aus. Hierbei greifen wir auf die Elernentfunktiör at ( ) zurück, die nn Falle eines Pufterüber- bzw. ’untedauH das Programm abbrieht. Alternativ kannst du hier natürlich auch nur auf die Version ml dem [ J-Operator zurückgreifen (bspw. toachgross [ i)) 154 scchs
Am Ende hat es doch noch Spaß gemacht So, nachdem du in diesem Kapitel dir Vorzüge der Klassen String und vector gegen- über den Zc-Sirings und Zc-Arrays genossen hast wollen wir das Thema gemütlich ausklin- gen lassen. Sofern du also keinen ganz besonderen Grund oder Befehl von oben bekommen hast, das Ze-Zeugs in deinem Programm zu verwenden, empfehle Ich dir. immer auf die dafür langzcilerprobten Klassen zurückzugreifen. In der normalen Praxis gibt es keinen Grund mehr umständlich auf das alte Ze-Zeugs zurückzugreifen. Hierzu nochmals kurz die Vorteile dieser Klassen Xl Über- bzw. Unterschreiten (Stichwort: Buffcr-Ovcrflow) des Puffers können vermieden werden, da die Klassen hierfür einen Schutzmechanismus anbieten (den man natürlich an kritischen Stellen auch verwenden sollte). •2J Speicher für neue Elemente wird immer automatisch zur Verfügung gestellt, was sonst mit einem ziemlichen Aufwand erstellt werden musste und wobei auch gerne Fehler gemacht wurden. •Xl Pie Klassen bieten enorm viele Elementfitnktionen. die sich auch noch sehr angenehm verwenden lassen. Auch Dinge wie der Umgang mit dem String-Endezeichcn wurden bet der String Klasse komfortabel gelöst. (<N<rtwen'*Üben) Vorteile, das alte Ze-Zeugs für neue Projekte zu verwenden, gibt es eigentlich kaum Natürlich sind die rohen Ze-Sachen immer um einen Tick schneller als die Klassen vector oder string Aber die Vergangenheit hat gezeigt, dass die vielen Fehler, die damit gemacht wurden, diesen Geschwindigkeitsvorteil häufig zunichte gemacht haben. Jetzt wird es nochmal Zeit für einen Test [Vchweräijr Aufjxbrl Erstelle ein Listing, welches einzelne Wörter über ein einliest und jedes Wort als einzelnes Element in einem Array speichert. Okay, ein Tipp dazu [Notierva/Üb«n] Hierfür kannst du die einzelnen Strings in einen Vektor packen (vector<string>) Die zur Lösung benötigten Elementfunktionen hast du bereits kennengelernt. Arrays* String** V«ktor#n* Strukturen und 2eig«r 155
Hier eine mögliche Musterlösung für diese Aufgabe, die vermutlich einfacher ausfällt, als du vermutet hast linclude «iostreao» iHtarbwudm linclude <vector> *1 wir beide Header linclude <string> ’1 usin8 namespace std; -2 Hiermit l*i«t vni die einzelnen lat main () Werte ein. < string wort; *2 vector<string> vecstrlng; *3 do { 4 3 Die Kl wen sind einem flexibel Daher rst cs auch gar kem Problem, ein leeres VCCtor mit der Klasse String Arttu legen. *4 Em einzelnes Won wird eingele^en (d.iher .iuch ein und nicht getline) ‘5 Das Wort wird am Ende des Vektors hirwujjefüx1 • Vorgang wird so lange wiederholt, bis der String wort nicht "fertig” lautet cout « "Wort cingcben (fertig-Endc): ein » wort; *4 if(wort ! "fertig”) vecstring.push back(wort); ‘5 }while(wort ! "fertig”)} cout « "Folgende Worte hast du eingeben for(8ize_t HO; i<vecstring.size(); !♦♦) *6 Auch die Ausgabe der einzelnen Strings im Vektor ist hier mit den bekannten fiardmit- teln ein Kinderspiel. Wesentlich einfacher kennst du e> meinen Elemente auch hier mit der neuen r.inge-bascd-fpr-Schleife mit ,, >t. fortauto &s : vecstring) enai, cout g <K endlj Ct.-—durchlaufen ( ________________________________ cout « vecstringli] « endl; ’6 } - return 0; Hf Itlnfeche Aufgabe] Im folgenden Codeausschnitt wurde ein Puffcrüberlauf provoziert. Du solltest wissen, dass es bei den Klassen vector und string eine Möglichkeit gibt, über diesen Pufferüberlauf das Programm zu beenden. vector<lnt> vccint(5); vcclnt[6) 999; // 11! Puffcrüberlauf !!l Und iö sieht es aus. «wenn tift Pufferuberlauf mit de* ( fonktioft atü erkanM wurde Völlig richtig! Später im Buch erfährst du außerdem noch, wie du diesen Fehler abfangen und behandeln kannst. |B*tohÄurt£j’Ldrtuög] Zur Belohnung kannst du jetzt deine Füße hochlegen, PuffrrubfrUuf wud frk*nnl Dieter 6aer $ ,/öufferoverflow tCfwiAHc cattcd iTtcr Wowing m$t^rKC of ’iW::Cut_€«f MhaUh vector:: Abort trap Dieter e^r- j (] 156 c*c.;t«i sechs
schuhe { *1 Stopp! Da habe ich etwas für dich. Wenn du unterschiedliche Typen verwenden willst oder musst, kannst du diese in einer Struk- tur zusammenfassen Anstatt also Folgendes zu verwenden: Verschluss; färbe; 3 Zirwrn mit gesch string marke; unsigned short EUgroesse; string String igenilxhtn pcichert ier kannst du "1 Mit dem Schlüsselwort St tuet und der s»ch öffnenden geschweihen Klammer leitest du die Zuwmmenlawung der unterichiedhclien Typen und des Namens schuhe ein. t n • • struct String marke J *2^^ unsigned short EUgfccssc; string verachlus string färbe; diwe vun£ der Struktur ich schließenden n Klammer und ebnem ikolon beendet __________ n die einzelnen S ktur- mitgl ieder fMumbersL in denej Date werden ......... alle bekamen Typen und ItlKii auch 4üit/, Typen nden. Nachdem du deine Typen in der Struktur zusanimengefasst hastr kannst du im Programm folgendermaßen einen neuen Schuh erstellen: schuhe einSchuh. nocheinSchuh, ichaucheinSchuh; Irrjyi» Strings» VflttQrgni St r u k t M *~en Ztagei 157
Gar kein Problem, hierfür kannst du auch ein Array verwenden. Entweder ein alles Ze-Array oder besser noch, du benutzt hierfür die Klasse* vector. Falls du es schon wieder vergessen hast, so kannst du einen solchen vector mit der Struktur schuhe anlegen: linclude <vector> vector<schuhe> vecSchuhe; Der Zugriff auidie einzelnen Slrukturmitglieder erfolgt dann über den Bezeichnerder Struktur, gefolgt vom Punkteoperator und dann dem Strukturmilglled {= struetbeZeichner.structmitgliedx Ansonsten erfolgt der Zugriff, wie du es von den Basis- datentypen oder den Klassen vector und String bereits kennst. Zum Beispiel: 1« cout cout cout cout schuhe einSchuh; *1 einSchuh.marke = " cinSchuh.EUgrocssc getline(cin, einSchuh.verschloss); *3 ein » einSchuh.färbe; ’3 Hirsch”; *2 : - 3»; *2 M Hier tfrh! die StruÜüWfr^bk mit (km Bwichner einSchuh einSchuh.marke « endl; *4 einSchuh.EUgroesse « endl; einSchuh.Verschluss « endl einSchuh.färbe « endl; B4 4 *2 Werte werden .in die 'Struklu/niH^liedcr über den Zuwriwn^vop^Mtar übergeben *3 Natürlich kjnntf du Jiixh über ein «' n q.mre Zeile (getline.i ode* cm einzelnes Wort von der Stanctafd- eingabt cifltaen. •4 Ähnlich einfodi funktioniert auch die Autfl.ibe über den Slream COUt mithilfe des AujgjbeoperaWrv « Natürlich kannst du das! Die einzelnen Werte kannst du in der richtigen Reihenfolge, getrennt durch ein Komma, zwischen geschweiften Klammem direkt bei der Dekla- ration wie folgt übergeben: schuhe einSchuh { "Birsch“, 38, "Schlupfschuh”, "Schwarz” ? >s Falls du hier auch gleich vector<schuhe> verwenden willst, kannst natürlich auch einen Teil deiner Schuhsammlung mit der neuen vereinheitlichten Initialisierungssyntax und einer {}-lnitiali- sierungsliste mit Werten versehen. Folgendermaßen kannst du bspw, gleich den vector mit zwei Paar Schuhen vorbelegen: vcctor<schuhc> ochrSchuhc { {•Hirsch", 39, "Schlupfschuh", "schwarz”} {"Addidas", 39. "Laufschuh", "blau"} }; 188 ucitfi sccms
Endlich echte Schuhdaten Nachdem du jetzt mit den Strukturen eine Möglichkeit kennen- gclerni hast, wie du verschiedene Datentypen zu einer logischen Einheit zusammenfawen kannst, ist es an der Zeh. dass du hier- für Code schreibst. (Schwierige Aufgabe] Erstelle zur folgenden Struktur ein Programm, mit dem du belie- big viele Schuhe In deine Datenbank einlesen kannst. Am Ende gibst du alle Schuhe aus der Datenbank auf den Bildschirm aus. struct schuhe { i > t string marke; unsigned short EUgroesse; string Verschluss; string färbe; iNotlr) Tipp: Verwende hierzu einen Vektor mit der Struktur schuhe: vector<schuhe> vecSchuhe; Hierzu eine mögliche Musterlösung linclude <lostreani> linclude «string? linclude <vector> using namespace std; schuhe { string marke; unsigned short EUgroesse; string Verschluss; string färbe; int malnO { vector<schuhe> vielcSchuhe; ‘2 “1 Di* Struktur wird juAefhalb der main Funktion enteilt, damit darauf global .luchvon anderen Funktionen zugegJ'ften werden kann, In diesem benpid ist dies allerdings noch nicht von Bedeutung. ‘2 Hier steht ein noch leerer Vektor lür Strukturen vom Typ schuhe . Arriyf» Strings«. Viktoren« Strukturen und Z«l9*r 159
*3 Hier sieht eine tempo- räre Struktur, die wir zum Einlesen verwenden. schuhe tempSchuh; ’3 int wähl; do{ cout « "-1- Schuhe hinzufügen^n"; cout « tt-2- Schuhe auflisten\nM; cout « "-3- Programmende\n"; cout « "Deine Wahl : '4 Da» Einlesen wird tikr über rin Mrnu in einer do whlle l Schleife rcahwrt *& Am Ende hangen wir dir Schuh- Struktur komplett gefüllt mit Daten ans. Ende des Vektors mit Strukturen* ein » wähl; ein.ignoref); if( wähl -• l) i { cout « "Marke : | getline(ein, tempSchuh.marke); ’5 cout « "Größe (EU) s ein » tempSchuh.EUgroesse; *5 ein.ignore(); cout « "Verschluss : getline(cin, tempSchuh.Verschluss); *5 cout « "Farbe ; getline(ein, tempSchuh«färbe); *5 vieleSchuhe.push back(tempSchuh); ’6 > eise if( wähl 2 ) < *5 Hier erhalten die einzelnen Strukturmitghcdcr unserer temporären Schuh-Struktur Ihre Werte über den Standardeingabe- stream ein cout « "\nDic Schuhsiurunlung deiner Freundin:\n\n"; for(5ize_c 1-0; i < vieleSchuhe.slze(}; ++i) *7h { cout « vieleSchuhe[i] .tnarkc « endl; *7 cout « vieleSchuhe[i)*EUgroesae « endl; -7 cout « vieleSchuhe[i]rVerschluss « endl; *7 cout « vieleSchuhe[ij ► färbe « ^Vntn11; *7 > }whlle( wähl !- 3 ); return 0; *7 Hier werden d»c Daten aller cingcgebencn Schuhe ausgegeben 160 c<c.;f L SC<Mt
• ' Sctiuhdattnbsint Dieter Baer $ ./schuhe -1- Schuhe hinzufügen -2- Schuhe auflisten -3- Programmende Deine Wahl : 1 Marke : Hirsch Größe (EU) : 38 Verschluss : Schlupf Farbe : braun -1- Schuhe hinzufügen -2- Schuhe auflisten -3- Programmende Deine Wahl : 2{J r./t$ ein.ignore() sc//? Das PrögrArtim für den S<hu*i-Fe1rtfhisun bei der AafFührung Das ist mehr oder weniger ein Trick, um ein übrig gebliebenes <—। Enwrj bei der vorherigen Eingabe aus dem Tastaturpuffer zu entfernen. Dieses übrig gebliebene <—1 Enter kann Probleme verursachen, indem es einfach für die nächste folgende Eingabe verwendet wird und du somit schon die übernächste Eingabe machst, weil die vorherige Eingabe einfach mit dem <—1 Enter gefüllt und übersprungen wird. INotid Leider lässt sich dieser Trick nicht überall so einfach anwenden. Der eine Compiler mag das ignore (). bei einem anderen Compiler kann es sein, dass du etwas anderes finden musst. Manche Compiler akzeptieren hierbei auch cin.getO, um unformatiert ein Zeichen aus dem Eingabepuffer zu ziehen. Die gute Nachricht allerdings: Bei allen Compilern, bei denen ich das getestet habe, hat es mit ein. ignore () geklappt. Array». String». Vaktpran, ttruttf.n u") Z*>9*r 161
Die gemischt Typen sind echt nützlich Freut mich, dass du der Held des Tages hei deiner Freundin bist. Trotzdem müssen wir hier noch das Thema aus- klingen lassen. Also bitte noch kurze Konzentration. (Eirnfjchr AulgatMj L _ r Finde die Fehler in dem folgenden Code- ausschnitt. struct Farbe < unsigned int rot; unsigned int blau; unsigned int gruen; }| Farbe mlschungOl 127, 127, 127; Farbe misehung02; cout « "Rot (0-254) ; ein » rot; cout « "Blau (0-254) : ein » blau; cout « "Grün (0-254) : ein » gruen; Ich denke, die Fehler im Beispiel waren offensichtlich und sollten dir keine Probleme mehr bereitet haben. 182 neun Stent
Hier die Lösung zu der einfachen Aufgabe *2 Um aui die Mitglieder der Strvklur ZUIUgreifen. fehlt natürlich die Verbindung zum Bezeichner. Eine Anweisung wie rot=100 bringt natürlich nichts, wed m ein valchet Rot in diesem Bezug gar nicht gibt Daher wud eine Farbe mlschungOI • ( 1271 127♦ 127 }| M Farbe mischungO2; cout « "Rot (0-254) : ein » mischung02.rot; *2 cout « "Blau (0-254) : ein » mi$chung02.blau; *2 cout « "Grün (0-254) : "; ein » mlschung02.gruen; '2 *1 Hier haben die geschweiften Klammern gelchlt1 eindeutige Ver- bindung mit mischung02. rot=100 benötigt. Nein, das geht auch so nicht direkt. Für einen direkten Ver- gleich von Strukturen musst du entweder die einzelnen Ele- mente miteinander vergleichen, oder du schreibst eine speziel- le Operatorüberladung für == und ! =. Aber das erfährst du noch später im Buch. Intern würde eine solche Operatoruberla- dung auch wieder nur über einen Element-ftir-Element-Vergleich realisiert. {Ztuel] Zu deiner Information sei hier schon mal gesagt, dass eine Struktur im Grunde nichts anderes als eine Klasse Ist, bei der auf alle Elemente von außen zugegriffen werden kann. Du könntest quasi auch öffentlich Funktionen als Mitglieder einer Struktur ein- bauen (wenn du könntest). In der Praxis wird aber nicht das Schlüsselwort struct für Klassen, sondern das Schlüsselwort dass verwendet. Strukturen sind im Grunde lediglich ein Sonderfall einer Klasse. Wieso? Weshalb? Warum? Erfährst du noch! iBelohnung/Ltaungl Zur Belohnung musst du deiner Freundin ein weiteres Paar Schuhe kaufen, damit die Datenbank erweitert werden kann. irriyjx ttringi* V»ktorpnl Strukturen und Z»Ä9»r 103
Eine Union ist wie die Struktur ein Verbund, um verschiedene Datentypen zusam- menzufassen. Auf den ersten Blick besteht zwischen einer Union und einer Struktur, abgesehen von den Schlüssel Wörtern union und Struct. kein Unterschied. Auch die Anwendung entspricht im Grunde den Strukturen. Allerdings geht eine Union anders mit seinen Verbündeten um. Während eine Struktur noch für alle Mitglieder nacheinander Speicherplatz reserviert. Ist eine Union da schon geiziger und reserviert nur Speicher für das größte Mitglied im Verbund. Die einzelnen Mitglieder werden bei einer Union überlappend angeordnet. Das bedeutet natürlich auch weniger Speicherbedarf. Im Crgrnsjlr -ru ein« Struktur stftd t*i eitw Union di« einwMw Mitglied« übeelipptnd jMgrondnft hi der Praxis bedeutet das allerdings, dass bei einer Union Jedes Mitglied auf derselben Anfangs ad resse liegt. Konkludierend bedeutet dies auch, dass bei einer Union nur ein Mitglied ver- wendet werden kann. 184 c«oit«i sccHt
Ehrlich gar keinen. Ich wurde dir In der Praxis sogarabraten, auf eine Union zu setzen, Zugegeben, bei maschinennaher Programmierung kann eine Union interes- sant werden. Die andere positive Möglich- keit. eine Speichcrstdle auf verschiedene Arten zu verwenden, ist ebenfalls nicht unbedingt relevant, well man hierfür auch abgeleitete Klassen verwenden kann. IZ""I| Mit C++11 wurden Unions erweitert und dürfen auch jetzt Daten von Typen enthalten, welche bspw. einen nicht-trivialen Konstruktor enthalten. Dies sei nur am Rande erwähnt, werl du ja wohl bis zum jetzigen Zeitpunkt mit solchen Begriffen wahrscheinlich noch nichts anfangen kannst. iHintrrgrundinfo) Die Nachteile einer Union durften dir zunächst noch nicht ganz klar sein. Aber wenn du später eigene Klassen erstellst, solltest du diese Unionen da nicht verwenden, da es hier beim Erzeu- gen und Zerstören zu Problemen kommt, weil der Compiler beim Freigeben eines Elementes nicht weiß, welches Element er denn zerstören soll. Ein weiteres Hilfsmittel, um eine Folge von ganzzahligen nume- rischen Aufzählungen zu definieren, steht dir mit dem Schlüsselwort enum (=Enumeration) zur Verfügung. Ein einfaches Beispiel: enum dass Wochcntagf SO, MO, DI ML. DO. FR, SA >? [z«ttei| Anstelle des Schlüsselworts dass darf auch das Schlüssel- wort struct verwendet wer- den. Kann dein Compiler kein neues C++11-enum.. musst du dass bzw struct weglassen. Arrjyi* Strings* Vektoren* Struktwran undi 2«ig«r 165
Hiermit tust du praktisch mit dem Schlüsselwort enum einen neuen Datentyp namens Wochentag definiert. Zwischen den geschweiften Klammern stehen jetzt. Jeweils getrennt durch ein Komma, die einzelnen Aufzählungen. Die Enumeration muss mit einem Semikolon abgeschlossen werden. Die Aufzählungskonstanten zwischen den geschweiften Klam mern haben einen festen Wert und beginnen bei der ersten Aufzählungskonstanlc (hier SO) mit 0 und werden, wenn nicht anders vorgegeben, um jeweils einen Wert erhöht (Inkremen- tiert). Somit ist S0=0. MO=1. DI = 2 MI = 3 usw. Hinter^rurtdin^J 0« Der Standardtyp der Aufzählungskonstante ist int. Aber dieser Typ kann jederzeit durch einen anderen integralen Typen überschrieben werden. Folgendermaßen kannst du bspw. unsigned int für die darunterliegenden Aufzahlungs- konstanten verwenden: » t enum dass Wochentag : unsigned int { SO, HO, DI, MI, DO, FR, SA }; Kann dein Compiler kein C++11, musst du diese Typenangabe weglassen. In der Praxis kannst du aber die Werte der Aufzählung auch ändern. Zum Beispiel: enum dass Wochentag : unsigned int { SO=1, MO, DL, ML, DO, FR, SA }; In dem Fall beginnt die Aufzählung am SO mit 1, MO ist 2 oder DI ist eben 3 usw. Innerhalb des entsprechenden Geltungsbereiches kannst du hierbei auch einen solchen Typ deklarieren: Wochentag wtO 1 (Wo-chentag::DI}, wt02 Wochentag11FR; if(wt01 == Wochentag::DI) { ff ... > unsigned int tag wtOl; II Achtung!!! Fehler llt 186 scchs
Hierzulande ist z. 15. der Wochenanfang der Montag. Dies kannst du auch in der Aufzahlung wie folgt wiedergeben: enum dass Wochentag : unsigned int { S0“7, MO”1, DI, MI, DO, FR, SA }; Jein hat SO den ganzzahligen Wert 7. MO den Wert 1, DI den Wert 2 usw. bis hoch zu SA mit dem Wen 6. F.gal. welchen Wert du jetzt für die einzelnen Konstanten verwendest, der Wert der nächsten Aufzählungskonstante, der keinen speziellen Wert zugewiesen bekommt, ist immer um 1 höher. tz«u«t] Theoretisch spricht übrigens auch nichts dagegen, wenn du zwei Konstanten mit demselben Wert belegst. Du kannst allerdings keine zwei Konstanten mit demselben Bezeichner verwenden* Richtig angewendet, können dir solche Aufzähl ungstypen das Programmieren (erleichtern. Anstatt bei einem Programm einfach nur tag=l zu schreiben, fällt cs einem doch viel leichter stattdessen tag = MO zu schreiben. Gerade bei if Überprüfungen oder in switch case Fällen ist so eine Aulzählungs- konstante wesentlich komfortabler zu handhaben: I Wochentag tagOl(Wochentag::MO}; switch(tag01) 1 < case Wochentag::MO: cout « "Heute ist Montag" « endl; break; case Wochentag::DI: ... 1 > - - IZettrl) Dass eine Zeile wie unsigned int tag = WtOl; nicht funktioniert, liegt daran, dass der enum-Aufzählungstyp von C++11 streng typisiert ist. so dass nur mit dem Namen des Aufzzahlungstyps darauf zugegriffen werden kann. Mit dem alten Ze- enum war dies noch möglich* Ebenso musst du jetzt mit C++11 für die Aufzählungstypen explizit mit dem Zugriffs- operator : i zugreifen. Eine Verwendung wie DI wäre Undefiniert und daher musst du hierfür Wochentag : :DI verwenden. Kann dein Compiler noch kein C++11- enum musst du den Zugriff mit Wochentag: : weglassen. Irriy;. Stringi. V»ktQr»n. Struktur« tfna Z»>9»r ft 7
lAbhfrl Vielleicht hast du bereits andere Codebeispiele mit enum gesehen und wunderst dich, dass das enum hier etwas anders beschrieben wurde. Nun, Ich denke, das Hegt daran, dass du das alte Ze-enum gesehen hast. Das alten Ze-enum hatte aber drei gravierende Probleme Diese drei Probleme wurden mit dem neuen C++11 enum dass bzw. enum Struct beseitigt. Du kannst damit andere integrale Typen definieren, du musst den Zugriffs- operator für den Zugriff auf die Konstanten verwenden und der Aufzahlungstyp ist nun streng typisiert! » Es wurde intern immer nach int konvertiert. w Der Bezeichner stand Im kompletten Bereich zu Verfügung "* Der grundlegende Typ einer Aufzählung konnte nicht verändert werden. Neu in C++11 wurden auch sogenannte Alias Templates (Schablonen) elngeführt. Damit ist es quasi möglich, mit dem Schlüsselwort usinfc Synome zu erzeugen. Das war zuvor "ht möglich. Nun. Templates sind hier zwar noch nicht das Thema, aber du kannst using auch für einfache oder komplexere Synome als Alternative für typcdef verwenden. using Supermarkt » clark_kent; anstatt typedef ClarkJcent Supermarkt t 168 SC<H$
Wie Clark Kent In die Rolle des Superman schlupft und Dr. Jekyll derselbe Typ wie Mr. Hydc ist, kannst du auch in C++ einem Typ ein Alter Ego verschaffen, welches ein und dieselbe Bedeutung hat. Relativ gerne wird diese Synonymie-Technik init dem Schlüsselwort typedef in Verbindung mit Strukturen an gewendet. Zum Beispiel: «•- '"I struct clark_kenc { string Sternzeichen; string geburtsort; bool kannfliegen; >; M typedef clark_kenc Superman_t; ’1 Hier tfehl die Struktur clark kent. ’2 Oie Struktur clark_kent bekommt ein Synonym Supermarkt. 3 Der Typ clark_kent kann jetzt auch mit Supernian_t verwendet weiden (beide Typen lind gleichbedeutend). Superman_t einSuperman; *3 vector<Supernsan_t> vieleSupennaenner; |Hint*r(rundinf«) Diese Vereinfachung von umständlichen Typnamen mit typedef wird übrigens auch mit der Klasse String dunchgeführt. Der Mädchenname von string lautet näm- lich in Wirklichkeit basic_string<char>. Natürlich a ^--kannst du die Synonymie auch für alle anderen Typen verwen- den. In der Praxis ist dies hspw. auch sehr hilfreich bei der Portierung von Programmen auf andere Systeme. Benötigst du z. B, einen 16 Bit breiten Typ, kannst du Folgendes definieren: typedef shorc lNTI6_t; Im Programm kannst du jetzt jederzeit INT16_t dafür verwenden. Sollte das Programm jetzt auf ein 16-Bit-System (.Hinten werden, auf dem ein int 16 Bit breit ist. brauchst du nur die Typdefinition zu ändern und musst nicht im ganzen Programm die Typen ändern. Also bei einem 16BiiSvsiem müsste cs dann lauten: typedef int INT16_c; Arrays» Strings» Vtktoribi Strukturen und 189
Leipziger Allerlei Die Überschrift hört sich leckerer an, als das Thema tatsächlich ist. Aber die Themen Unionen. Aufzählungen und die Synonymie ist doch schon ein ziemlicher Eintopf verschiedener Zutaten, die wir hier einfach mal zusammenmanschen und bei denen wir schauen, wie es schmeckt. •1 Hier steht die Struktur, in dec du die Werte der Farben Rm. Grün und Blau *pe ehern Icanrnt. Die Typdelinition RGB24_t wurde direkt bei der Erstellung der Struktur durchgeführt, was ohne Problem möglich ist. [Schwierige Aulgabe] Schreib eine Union, welche Farbwerte speichert. Verwende als Typ für den Farbwert einmal eine Struktur mit dem Namen RGB, um Rot, Grün und Blau als Ganzzahl zwischen 0 bis 254 zu spei- chern und verwende als zweiten Typ in der Union die Farbanga- be als hexadezimalen Wert. Erstelle außerdem mithilfe von typedef ein Synonym namens Farbe_t für den Farbwert. Hier eine mögliche Musterlösung der Aufgabe typedef struct RGB ( *1 unsigned int rot, gruen, blau; } RGB24_t; *1 typedef union Farbe { *2 RGB24_t rgb; *3 unsigned int hexRGB; *3 } Farbe t; ’2 Hier steht deine Union. Auch hier haben wird, gleich die Typdefiniticm von Färbest .in Ort und Stelle durchgeführt. [Code Wenn du die Typdefinition gleich an Ort und Stelle übernimmst, wie: *3 Denk daran. eine Union kann nur einen der beiden Werte wrwenden. weil sich beide Spctchcfbewhe überlappen. Du kannst entweder nur die Farbdaten mit der Struktur RGB 2 4_t oder den ganzzahligen typedef struct RGB { Weu hexRGB zum Speichern unsigncd int rot* gruen, blau; } RGB24 t; Dann kannst du auf den Bezeichner RGB gleich verzichten und stattdessen schreiben: typedef struct { unsigned int rot, gruen. blau; } RGB24__t; 170 UqU«! SECHS
Itinfach* Wenn dir das zu komplex war. schreib jetzt einen kleinen Code, in dem du jeweils einen Typ Farbe_t anlegst und einmal die Farbangabe über die Struktur und dann über den Integer mit Werten belegst. Um von der Union auf die Mitglieder der darin enthaltenen Struktur zuzugreifen. musst du den Punkteoperalor zweimal verwenden! Hier die Lösung zu dieser einfachen Aufgabe Färbest eineFarbc; *1 eineFarbe*rgb.roc 255; eineFarbe^rgb.gruen 100; elneFarbe*rgb-blau • 127; Farbe_t nocheineFarbe; *2 nocheineFarbe.hexRGB '1 Um tvier auf die Struktur rgb lugrrrtrn tu können. du natürlich erst den Weg über dir ima eineFarbe dem Punkteoperator gehen. Erst dann kannst du über die Struktuf rgb auf die Farbe zugrerien Du musst quasi einen Zugriff um zwei Ecken machen, weil d« Struktur p innerhalb der Union verschachtelt Solche Ver5cha<h(c4ungen können natürlich auch mit reinen Strukturen gemacht werden und sind nicht dtd Unionen beschränkt Oer Zug*# bleibt auch hier derselbe. 2 Hier steht die zweite Möglichkeit, den Weit an das andere Mitglied der Union ru übergeben (Notiz) Die Verwendung von typedef an Oft und Stelle, wie hier verwendet, mag zwar sehr komfortabel sein, aber in der Praxis durfte wohl doch meistens die Definition in zwei Schritten zum Einsatz kommen. Trotzdem sollte hier noch gezeigt werden, was möglich ist. Du hast cs sicherlich gerade selber aus- probiert, weil du fragst. Sicher bist du verwundert, dass sich dein Compiler nicht beschwert und das bedingungslos mitmacht. Das Ist eben eine weitere Gefahr bei einer Union. Weil diese nur rohe Daten speichert, merkt sie sich nicht, im Gegensatz zu einer Struktur, welche Variante cs sein soll. Es ist also immer möglich, auf die anderen Mit- glieder der Union zuzugreifen, egal wie sinnlos das Ergebnis am Ende ist. irrjyj. Strings. Vtlttorgni Strukturfn und Ztag + r 171
typedef Schrödinger hat_alles_kaplert_t Nachdem du auf den letzten Seiten wieder einiges hinzugelemt hast, wollen wir das Thema nochmal ein wenig zusammenfassen. Folgendes hast du jetzt erfahren: Q Auf Unionen mit dem Schlüsselwort Union kannst du in C++ verzichten, wenn es dir nicht um Maschinennähe geht. Speichereinsparung ist bei der ge- wöhnlichen Programmierung nicht wirklich ein Argument für die Unionen. Als bessere C++-Alternative kann ich dir die abgeleiteten Klassen empfehlen, die du unbedingt noch kennenlernen solltest. Um aus ungriffigen und schrumpligen Namen ein einfacheres Wort zu machen, kannst du neue Synonyme mit typedef und using (seit C++11) daraus erstellen. Auch bei der Portierung von einfachen Basistypen auf unterschied- lichen Systeme kann ein solches Synonym an der richtigen Stelle unglaublich hilfreich sein. 13 Der Aufzählungstyp enum macht sich gut, wenn du ganzzahlige Konstanten einem Namen zuordnen willst. (Zettel! Eine Aufzählung bzw, Enumeration ist übrigens auch ein vollwertiger C++-Typ lEkrfuht Aufgabe) Eine kleine Aufgabe für den Aufzählungstyp könntest du noch lösen. Sie dir den folgenden Codeausschnitt an. und verbessere diesen, indem du einen Aufzählungstyp dafür verwendest. int Ampelzustand 1; Int Ampelfarbe 1; if( Ampelzustand 1 I/ 0 = aus II 1 - an II 1 - rot II 2 = gelb II 3 • grün && Ampelfarbe 172 SCCHS
Der Code schreit ja geradezu nach einer Enumeration. Gerade solche Dinge wie Ampelf arbe= 1 oder Ampelzustand= 1 sind sehr verwirrend und fe hieran fällig. Zumal hier auch Dinge wie Atnpelf arbe=99 möglich sind. Hier die etwas bessere Lösung mit der Enumeration enum dass Ampel { an»O, aus*l, rot“l, gelb, gruen }; *1 Ampel AmpelzustandfArnpel::an}; *2 Ampel Ampelfarbe(Ampel::rot}; *2 lf( Ampelzuscand •• Ampel::an && Ampel färbe Ampel::roc ) *2 < > Ampel::Ampelfärbe • 99; // Nicht erlaubt ’3 1 Hkcr rtcht drf neue Aufahlungityp Ampel "2 Den Typ vetwenden wir auch für d>e Zuweisungen und Überprüfungen 3 Solche Fehler können jetzt ^^^^^Bvermieden und vom Compiler bemängelt werden. |Ne'i*rtft?Übeft| Eine nützliche Funktion habe ich hier noch für dich. Und zwar geht es um der» Codeausschnitt von: typedef short INT16_t; Sinn und Zweck ist es natürlich, dass du hier mit INT16_t einen 16-Bit breiten Typen anbieten willst. Aber du kannst ja nicht sicher sein, ob es eben 16-Bit sind. So kannst du dann bspw. auch folgendes verwenden: typedef Int INT16_c; falls du einen Rechner mit einer 16-Bit-Wort breite vor dir hast und int eben mit 16-Bit implementiert ist. Unabhängig von diesem Thema gibt es eine Funktion, womit du dir bereits zu Übersetzungszeit zusichern lassen kannst, dass dein Typ auch tatsächlich die nötige Größe besitzt (hier 16-Bit oder eben 2 Bytes). Hierfür kannst du das neu in C++11 eingeführte staticasscrt wie folgt verwenden: typedef Int INTlfet; statlc_assert( slzeof(INT16_t) !- 2, "Achtung lNT16_t hat nicht die passende Groesse”); Die Syntax zu Statlc_assert lauetet folgendermaßen: static_assert( ausdruck, fehlermeldung ); Wenn ausdruck false zurückgibt, wird der Code vom Compiler nicht übersetzt und gibt den Fehler Fehlermeldung aus. Irrjyi» Strjnqy» V»lctortni urnd Zeiger
Weißt du, wie viele Stemleln em Himmel stehen? 10D1C11- Wenn du dir eine Insel kaufen willst, wirst du wohl kaum mit Kisten voller Geld zum Bezahlen aufiauchen. Die Geldbewegungen finden In der Regel eigentlich nur rein vir- tuell über dein Bankkonto auf den Cayman-Inseln statt, wenn du etwas zu verbergen hast bzw. reich bist, oder eben über deine Hausbank. Bei mir ist es leider auch nur die Hausbank, die mir aber den Kredit für meine Insel nicht geben will. Es <ibt mehrere Wege, elfte Insel tu Miulett. entweder min vcMeppt Bitfeld mit tkh hrtum oder pmö Utagt eine Überweisung. Ähnliches gibt es auch in €♦* mit den Zeigern. Anstatt hier haufenweise Daten hin- und herzuschaufeln, ist cs auch möglich, nur mit der virtuellen Anfangsadresse dieser Daten zu arbeiten. Das ist im Gründe ganz einfach! Statt der Daten speichen ein Zeiger nur die Anfangsadresse der Daten. Wenn du z.B. eine Struktur oder ein Array in einem Programm verwendest, bekom- men diese Objekte eine feste Adresse im Speicher zugewiesen, Mithilfe der Zeiger kannst du auf diese Adressen und somit auch auf den Inhalt zugreifen. Einen solchen Zeiger kannst du so deklarieren: *1 Den Zeiger «kennst du am Sternchen zwischen dem Typ und dem Zeiger- namun. De - TYP des Zeigets ist strengstens typisiert Das helfit, der Typ des Zeigers miM dem Typ der D.i'en entsprochen. auf die d.r *.i‘f verweht. Ak Zeiger_Name k.uinst du jeden gültigen ßoirichnrr verwenden, wie du das schon von den Datcfitypm her kennst TYP *Zeiger Nanc; ‘1 174 sechs
IHinlfrgrundinta) Anders als die Basisdatentypen hängt die Speichergröße eines Zeigers nicht vom Typ des Zeigers ab. Ein Zeiger vom Typ char* hat somit dieselbe Speichergröße wie ein Zeiger vom Typ long*. Ein sizeof(Zeiger) belegt somit immer dieselbe Größe (eine Wortbreite), letztendlich muss ein Zeiger ja schließlich nur eine Adresse speichern können. Dinge wie der Vergleich zweier Zeiger mit den Vergleichs- operatoren, die Subtraktion zweier Zeiger oder die Addition und Subtraktion eines Zeigers mit einer Ganzzahl verlangen nach einem Typ bzw, ließen sich ohne eine solehc Typisierung nicht realisieren. Damit dein Zeiger zu den Daten findet. musst du diesem natürlich den Wohnort übergeben. Das ist eine heikle Sache, denn bei einer falschen Adressübergabe kann dieser total ausflippen und einiges kaputt machen. Ja, die Zeiger sind sehr empfindliche Wesen und sollten immer mit Vorsicht in die Welt gesetzt werden. Hierzu ein Ausschnitt, wie du einem Zeiger den Wohnort von anderen Daten übergeben kannst *1 Hier steht ein *2 Hier stehen die Dal en vom Typ int ZeißCf vom Typ int *zeiger_auf_int; *1 int inc_dacen 2323; *2 zeiger_auf_int = ünt_daten; *3 "3 (he Zuweisung der Adresse intdaten an den Zeiger zcigCrdufint Ganz wichtig ist hier der i-Adress-operator Ohne den Sc würdest du nicht die Adresse von int_daten an den Zeiger übergeben, sondern den Wert 2323. Das ist insofern dramatisch, sobald du den Zeiger mh dieser Adresse verwendest weil du nicht wissen kannst, wer oder was auf der Adresse 2323 wohnt. Schon vergessen? Zeiger wollen Adressen haben und nicht deine Werte! Arrays* Strings* Viktortn, Strukturen und Zeiger 175
Miger_AufJnt = Jdnt.datan; z«ij€r_W_rrtt = mtjtfltav mf «taten <*f <M k_______________________________________________________________________________J •ekom<nt d<r Ztig«r kein« Adttttt, wird <R* weitere Verkirf des PiöfrAfftmi J-urrt Ü-Ii, weil mw nicht werft, wjh drin il1. Nach der Wohnortbesilmmung des Zeigers bleibt dann noch der indirekte Zugriff auf die Daten über den Zeiger. Auch hier sollte man nicht unbedingt gleich Thors Hammer auspacken und wissen, wie es geht, Um also auf die Daten des Speicherobjektes über den Zeiger zuzugreifen, wird das Sternchen verwendet! •1 Uber den Indircktions* operator kannst du jetzt (in-> direkt auf den Weit von intdaten zugreifen, in dem fall würde int__daten den neuen Wert 1212 erhalten. *zeiger_auf_inc 1212; *1 |H«fltergfundinfo) er ‘-Operator wird auch als Indirektioni- oder Verweisopera- tor bezeichnet, und der Zugriff mit diesem Operator ist auch unter dem Namen Dereferenzierung bekannt. ss SSSS ssss SSooooo ooooooo sss sss ______sssss_sssss ssssssssssss ssssssssssssssssssssssss —ssssssssssssssssssss ssssssssssssssss —SSSSSSSSSSSSSSSSSSSS SSSSSSSSSSSSSSSSSSSSSSSS sssssssssss Die Wohnorte von Datentypen und Zeigern selbst kannst du mit dem & Adressoperator verwenden. Angaben wie &int_ datenodei &zeiger_auf_int verraten dir die fixe Adresse im Speicher. Den Inhalt, der bei einem Zeiger Ja eine Adresse ist. bekommst du ohne den Adressoperator zurück. sssss ooooooo SSSSSS ssfss^ ooooSSS 0000 oooo 0000 oo _•_______SSS ______SSSSS sssssssssssssss sssssssssss sssssss ssss sss J sssss »ssssssssssssss sssssssssss _sssssss ssss ssss sss 178 stCHt
Stemenkunde Wenn die Sternchen bereits in dein Hirn gefunden haben, wollen wir jetzt schauen, dass diese darin auch Platz machen, bevor sjdi die Swtapsen wieder auflösen. Daher jetzt ein Zeiger-Listing linclude <lo5trean&> using namespace std; Der Zeiger erh.Ut den Wohnort von int^daten Damit cs auch die richtige Adresse *st. musst du den Adressoperator & «mt- angeben. "2 In beiden Fallen wird der Wert von int_datcn ausgegeben. Mithilfe d« Sternchens vor dem Zeiger kannst du indirekt .iuf den Wert von int^daten zugrelfcn, Da« 7<i^r4d<m6rtiUji- Iwniprogramm bw der A^rtfvhrurtg int mainO { int *zelgerjiuf_lnc; int intdaten » 2323; zcigcr_auf_int - &lnt daten; cout « *zeiger_auf_int « endl; cout « int_daten « endl; *2 *zeiger_auf_inc 1212; cout « *zeiger_auf_int « endl cout « int daten « endl; *4 II Adressen ausgeben cout COUt « cout « cout « return 0 zeiger_auf_int « endl; &zeiger_auf_int « endl int_daten « endl; &int_dacen « endl; - ?i indirekte Zugriff auf die Daten von _nt_datcn mitiirtleder Deroferenzierung hj<iki«omert rutüdich auch schreibend. Hier wird def Wert von int_daten indirekt von 2323 auf 1212 grAnCtert e Änderung au? int^daten auf _‘liy>ctrt uUden Adwsoperator u?r den Zc^cr. wiro de* Wohnort llj Zeigers efc! Dieter Baer S ./zeigerdemo 2323 2323 1212 1212 0x7fffSfbff974 0x7fff5fbff978 1212 0x7fff$fbff974 Dieter Baer $ MilM ich dazu noch w.is 1 Wer h.it eigentlich die S|Nechbl.uc dahin 7- gemacht? Str lng$ Mithilfe des AtltcsSöper.1!Or1 rar den Daten bekommst du dei Wohnort der Daten '>• >pi> eher, wor.iul di" 2e<gi'i zeige r_auf_int. ja auch soiht tOFtn, ItP-t, k tur*n
Bei einfachen Basisdatentypen ist das in der Tat eher Unsinn. In der Praxis wirst du auf die Zeiger aber noch öfter stoßen, als dir lieb ist, weil sieh viele Dinge mit Zeiger einfacher und effizienter gestalten lassen. Lass es einfach auf dich zukommen. Merk dir einfach, dass du mit Zeigern auf die Wohnorte von anderen Objekten im Speicher schauen kannst. Ze-Zeugs, Zeiger und wo soll das hlirfiihren...?! Sicherlich erinnerst du dich noch an das alte Relikt mit dem Ze- Zeugs. Das müssen wir hier nochmals der Vollständigkeit halber ausbuddeln Cbrrrr"). um hier die Zeiger näherzubringen. Und zwar betrifft das die Verwendung von blab la [ i ] und *(blabla + i). Beides hat nämlich dieselbe Wirkung. Gleiches gilt ftir blabla[0] und *blabla. Solche Dinge haben in der Vergangenheit oft für Verwirrung gesorgt, näm- lich. dass Ze-Arrays bzw. -Strings und Zeiger scheinbar dasselbe sind. Das ist allerdings nicht so. Es ist nur so. dass der Compiler einen Zugriff tnil [ ] letztendlich bei der Übersetzung ohnehin zu einer Zeigerversion umwandelt. Noch genauer Der Inhalt eines Ze-Arrays hat beim Programm beginn schon einen festen Wohnsitz, der nicht mehr verschoben oder in der Größe verändert werden kann. Der Inhalt eines Zeigers hinge- gen kann die .Reise nach Jerusalem" spielen. _______ Ja, das geht. Du musst hierbei nur dem Zeiger die Adresse des Arrays übergeben, Aber auch hier gibt es wieder Verwirrung 178 SECHS
Neben der Möglichkeit, das erste Element mit &blabla [ 0 ] an den Zeiger zu übergeben, kann hier auch nur blabla an den Zeiger übergeben werden, weil der Name des Arrays ohne die eckigen Klammern einen konstanten Zeiger auf das erste Element darsielIt. ItiafKh* Aulgitw] Dir hier ein Beispiel abzudrucken, wäre zu langweilig. Daher fin- dest du hier erneu kleinen Codeausschnitt zur „Reise nach JerU' salem”. Versuche herauszufinden, welcher der folgenden Werte im int-Array j crusalcm nicht auf 0 zurückgesetzt wurde. int jerusalem|5) = { 1,2,3,5,8 }; int *reise_nachl, *reise_nach2, *reise_nach3; । rcise_nachl = Jerusalem; । reise_nach2 &jcrusalem[2); reiae_nach3 = Jerusalem + 4; *reise_nachl =0; i ♦rcisc_nach2 «0; *reise_nach3 =0; i *(reise_nach2-1) =0; Hierzu die Lösung und Erklärung, warum das vierte Element im Array die „Reise nach Jerusalem“ gewonnen hat *t DcrZe^er reise_nach 1 bekommt die Adresse des ersten Elementes von j erusa lem Du hättest hier auch / &jcrusalcm[0J '•senden können 2 Der Zeiger reise_nach2 erhalt die Adresse des dritten tlernentes «n Jerusalem int Jerusaleni(5) • { 1,2,3,5,8 ); int *reise_nachl, *reise nach2, *reise nach3 relse_nachl Jerusalem; reise_nach2 fcjerusalem(2]$ *2 reise_nach3 Jerusalem + 4; *3 So geM's juchl Der Zeiger reise_nach3 bekommt die Adresse des letzten Elementes. Spätrslens jetzt sollte dir auch die Wichtigkeit des Typisierens von Zeigern cinlcuchtcn. Ohne die richtige Speichergroftc des assoziierten Typs könnte das Nachfalgcrclemcnt über den Zeiger nie erreicht werden Arrjy;. Strjnqy. V»ktortn. Strukturen und Z»ag*r 179
'4 Hiermit werden irxiirckt üb Cf (kc ZclRff *' — eise_nachl. reise_nach2 und reise_nach3 dw erste, dritte und fünfte Element im A/my Jerusalem auf 0 gesetzt *reise_nachl 0; *reise_nach2 • 0; *reise_nach3 « 0; *(rcisc_nach2-l) int *reiste_nach for($ize_t l«0; i < 5; !♦+) < * cout « *(rCistc_nach + i) « endl; > *6 Ober einen Ähnlichen Weg findest du hierzu noch ein — . pur Zeilen, die dir den Inhalt de* einzelnen Array» Elemente ind rekt über den Zeiger reiste_nach .1 uneben Mit dem C»* 11 Slil kannst du d< einzelnen Elemente noch bequemer ausgeben: for(auto &j : Jerusalem ) cout « j « endl; Zeiger auf nichts...! • H »er springen wir zunächst in der Ktanmerung mit dem Zei er reise_nach2 ein ttement zurück zum zweiten Element in Jerusa indirekt (mit dem Sternchen! den Wert 0 zu Oie Klammer jr»; r» r eine höhere Priorität dis das Sternchen und wüd <Mh..r vorher avsgetübrt! In der Praxis solltest du natürlich immer den Rückgabewert eines Zeigers überprüfen, speziell ob dieser einen gültigen Wohnort enthalt. In C++ kannst du diese Überprüfung mit dem Schlüsselwort nullptr machen: INotill int *nichts; if(nichts « nullptr) // Alternativ auch: if((nichts) < cout « "Fehler11!'m’1; } Der nullptr wurde neu mit C++11 emgefuhrt. Zuvor wurde hier eine Überprüfung auch 0 oder QL (long) gemacht. Der nullptr erhöht natürlich die Lesbarkeit eines Quellcodes enorm, da du sofort erkennst, dass es sich hierbei um einen Zeiger handelt. Bei 0 oder ÖL konnte man oft nicht auf dem ersten Blick erkennen, ob es sich hierbei um den Zeiger 0 oder den Wert 0 gehandelt hat’ Altes Zeug lAthtUrt^l Das Makro NULL aus andere Mal statt 0L für Zeiger verwendet wird, ist eigentlich ein altes Ze-Zcugs-Makro und sollte in C++ nicht mehr verwendet werden’!! 180 Upit.l sechs
Wo geht*s hier zur „MIlky Way*? Zugegeben. die Zeiger hüben es in sieh, und wie du sicherlich bemerkt hast, sind diese ziemlich mächtig. Ähnlich wie Agent Smith in der Matrix Neu Jederzeit au (spüren konnte, kannst du mit den Zeigern ziemlich viele seltsame Orte in deinem Programm aufsuchen. Diese Freiheit ist allerdings oft auch ein Fluch, und damit kannst du dir schnell mal ins Knie schießen. Es bedarf also schon einer gewissen Zeit und Übung, um die Kontrolle Ober die Matrix, äh Zeiger ausüben zu können. Blaue oder rosa Pille? Okay, Mr, Anderson, kommen sie zurück nach Zion! Ihre Missi- on lautet zunllchst noch, die Zeiger in den Griff zu kriegen. Wol- len wir mal sehen, ob du der .Auserwählte* bisl. (Eiatahe Aufgabe] Übergib der Variablen daten indirekt über den Zeiger was mach ich den Wert 3.1415. float *wasmachich; float daten; float array[5j = { 0.0 }; Hier die Lösung wasmachlch =tdaCen; *w»smachich 3.1415; Prima! Ich bin froh, dass du diesen Vorgang bereits verstanden hast. Aber du bisl noch nicht fertig! Noch eine weitere Aufgabe dazu ISchwkfige AtolgibeJ Übergib dem ersten Element vom Ze-Array array den Wert der Variablen daten. den du eben mit 3.1415 initialisiert hast. Übergib außerdem dem letzten Element im Array den Wert 33,33, Zum Schluss gibst du noch die Adressen und Werte der einzelnen Array-Elemente aus. Für die Wertübergabe und die Ausgaben darfst du nur den indirekten Weg über den Zeiger wasmachich verwenden! Ich würde das folgendermaßen machen ...
wasmachich = array; ’1 *vasmachich dacen; '2 *(wasmachich+4) 33.33; *3 for(si2e_t i"0; i < 5; i++) { •1 Der Zeiger wasmachich bekommt den Wohn- ort vom ersten E/cmcnt von array *2 Da der Zeiger die erste Wohnung in der Reihenhaushallte array \pr»chert, ubetgeben wir diever Wohnung indirekt den Wert vor daten cout « wasnachich+l « « *(wasjnachich+i.) - « endl; ?4 ’3 ZunAchs» gehen wir mit dem Zeiger In der KUmmcrung vier Wohnblöcke bei den Reihenhäusern weiter und Das Programm bei der Ausführung Ztig<fdc*möniifauon Dieter Baer $ ,/zeigersalat 0x7fff5fbff950 0x7fff5fbff954 0x7fff5fbff958 0x7fff5fbff95c : 0x7fff5fbff960 Dieter Baer $ [ : 3.1415 : 0 : 0 : 0 : 33.33 übergeben der letzten Wohnung von array den Wert 3333. •4 Dio deckte Ausgabe der Adresse erfolgt über den Zeiger, ohne Sternchen, plus der Haus- nummer Oer Inhalt der Adresse wird, wie gehabt auch zunächst über die Adresse plus Hausnummern in Klammern angcstcucrt. gefolgt vom indirekten Zugriff auf den Wert ub<v das Sternchen Bin wirklich beeindruckt von dir. Hätte dir das nicht zugetraut. Zugegeben, die Aufgaben haben mehr oder weniger keinen Sinn und waren im Grunde nur Denk- sportaufgaben. Aber diese Aufgaben sind nützlich, um die Motivation von Zeigern besser kcnnenzulernen. IMoiitren/LJbenl Im Grunde sind die Zeiger gar nicht so wild, wenn du weißt, wann du was machen sollst. Neue Besitzer von Zeigern haben oft Probleme, wann und wie sie das Sternchen * (indirekter Zugriff) oder den Adressoperator & verwenden oder nicht verwenden sollen. Wenn du damit keine Probleme mehr hast, sollte es auch keinen Daten-GAU geben |Belohaufi£/L6fUflg] Prima, jetzt hast du dir deine Pause verdient. Ich weiß, es brennt dir schon den ganzen Abschnitt lang unter den Nägeln, dir wieder mal die Matrix-Trilogie auf Blu-ray reinzu- ziehen. Also: „Enter the Matrix!“ 182 X.okt.l SECHS
Bisher hast du die Zeiger nur als Sparring-Partner verwendet und sinnlos damit hemm gespielt. Daher ist es jetzt an der Zeil, dass du dich den echten Herausforderungen stellst. Lei ’ s get ready io RAMble ... Bisher hast du dich nicht darum geschert (oder kümmern müssen), woher der Speicher für deine Daten gekommen ist. Du hast zwar gewusst, dass da was war, aber du konntest nicht genau sagen, was. Wenn du einen Typ oder eine Reihe von Typen mit [ ) verwendet hast, hast du bekommen, was du wolltest. Was aber, wenn dir die Anzahl der Typen nicht ausreicht? Was ist, wenn du z. B. ein Array wir einTyp [ 10 j hast, aber eben 20 Elemente brauchst? l*/c> *z*j*v/*^, ic£ s&a. vector ctfcLz /ex/ i/Cti. string/ Hmpft Pssstl Im Grunde hast du Ja natürlich Recht! Für die grundlegende Programmierung bietet C++ mittlerweile mit den Klassenbibliotheken alles an, um sich nicht auf einer solch .tiefen' Ebene rumtreiben zu müssen. In der Standardbibliothek sind neben vector und String noch viele weitere solcher Klassen vorhanden. 42 Hier ddi GcgcnstiKk zu dynamischen Arrap Hiermit fordern wir qu.wi n Elemente Speicher «Ml Typ .Tn Die Aniara^idrew defr Wohnblock'. erhält ruturfich unver Zeiger. der dann auch gleichzeitig die Hamvcrw^ltung übernimmt iHinUrgrundinlo] An dieser Stelle muss man schon sagen, dass es C++ einem wirklich leicht macht mit den Standard- Containerklassen. Statt .wie soll ich implementieren" ist in C++ vieles .bereit zum Verwenden“ Viel du musst noch lernen, junger Jedi! Aber intern verwenden diese Klassen natürlich auch die dyna- mische Speicherverwaltung, um erneut Speicherplatz für sol- che Elemente zu erhallen- Und wenn du einmal eigene Klassen entwickeln musst, kann es sehr hilfreich sein, wenn du weißt« wie man selbst zur Laufzeit dynamisch Speicher anfordem "1 Hiermit forderst du vom System so viel Speicherplau an, wie der angegebene Typ hinter dem Operator new benötigt. Bei erfolgreicher M । Ä Reservierung wird die Anfiingwidresie dei Speicherplatzes zurüdcßegeben. Dieser Wohnort ^B wird natürlich von einem Zeiger verwaltet! Um also Speicher eines bestimmten Typs zur Laufzeit des Programms anzufordem, steht dir der Operator new zur Verfügung: Typ* Zeiger new Typ; ’1 Typ* Zeiger - new Typln]; *2 Irriy;. Strtngj. V«l<tor»n, tt r u k t M’~»n wna Z»ig*r
double *dZeiger; dZeiger new double; ’1 *dZelger - 3- 1415g *1 cout « *dZeiger « endl; *2 $ . oder aber über ,Zeiger- schretbweise“. Ach, komm schon, SO schlimm ist dds auch wieder nicht Guck dir folgende Codezeilen an, dann solltest du es besser verstehen können: •1 Hiermit hast du zur Laufzeit einen Speicher für ein double •sizcof (double)) reserviert. Der Zeiger dZeiger verweist auf den Wohnort (ein tinfarnilienham) des reservierten Speichers. double ^Hausverwalter; Hausverwalter new double [31; -3 Hausverwalter(Oj • 1.1; Hausverwalter[Ij 2.2; *4 *(Hausverwalter * 2) “ 3*3; *5 ‘2 Der Inhal: dei neuen Wohnortes bekommt den Wert 3.1415 njgewie$en. Wie immer kanmt du über das Sternchen Indirekt auf die Wohnung jugreefen "4 Zugreilen kannM du auf die einzelnen Wohneinheiten mit dem Indexoperalor ... *3 Hiermit rc$e*v»cr$t du dee« Clemente vom Typ double (3*(sizeof (double))) Der Zeiget Hausverwalter -.erwei« 4u<den Anfang des WohnMotks. Zwar Ist auf dem reservierten Speicher kein grüner Punkt, aber auch hier kommst du nicht um die gezielte Müllbeseitigung herum. Den Speicher, den du mit new anforderst, bekommst du von der Speicherhalde (engl. Heap): Frischer RAV von der Spdüchtf'hild* Müllproblem No. 1 Es ist besonders wichtig, dass du den Wohnort eines dynamisch reservierten Speichers nicht verlierst. Den kannst du bspw. verlieren, indem du den Zeiger auf eine andere* Wohnung verweisen lässt. Bezogen auf das Beispiel mit den Zeigern dZeiger und Hausverwaltung könntest du diesen Blödsinn folgendermaßen erzwingen: dZeiger - Hausverwaltung: 184 c«c.u«i sich*
Das Problem an dieser Codezeile ist. dass jetzt beide Zeiger auf denselben Wohnort .zeigen und der ehemalige Wohnort von dZeiger jetzt verwaist ist. Das Speicher- objekt ist zwar nach wir vor auf der Spcicherhaldc vorhanden, aber es gibt keine Mög- lichkeit mehr, darauf zuzugreifen. Du hast hiermit ein Speicherleck (Memory IcuJl) erschaffen. Sperch*r-HaMe Ein kljHiiuhet Spekhef- tet k wurde p*ovor*e<V dZeiger = KäuSverwaffun^ Sptichtrieek Müllproblem No.2 Ein zweites Müllproblem entsteht, wenn du immer nur Speicher vom System haben willst und diesen nicht mehr frei- gibst. obwohl du diesen gar nicht mehr benötigst. Wenn du also mit der Arbeit der dynamischen Objekte fertig hist, solltest du diesen Speicher bei Nichtgebrauch wieder an das System zu rück geben Hierzu findest du mit delete bzw delete [ ] passende Gegenstücke zu new bzw. new ( ]. Zum Beispiel: delete dZeiger; *1 delete () hausverwaltung; *2 "2 Bei de-r Freigabe eines A/rays müssen zusätzlich noch die eckigen Klammern mit angegeben werden. Ohne die eckigen Klammern wurdet du nur d.n ertfr Eleme-ril im Array freigebcn* •1 Hiermit wir das Spekhetobjekt fieigegeben, aut dai der Zeiger dZeiger verweist. |Zrll*l] An dieser Stelle möchte ich gleich noch Werbung für die neue C*»11- Klasse unique_ptr machen, wo du dich gar nicht mehr um die Frei- gabe von Speicher kümmern musst, weil der automatisch delete auf- tuft. Diese klugen Zeiger werde ich dir später im Buch demonstrieren (HifttergruHdintal Es ist nicht festgeschrieben, wie delete den Speicher freigibt. Garantiert ist nur. da« der Speicher als .frei" auf der Speicher- halde markiert wird und von dort auch für künftige Verwendungen wieder angefordert werden kann. Und weil der Speicher eben nur als „frei“ markiert wird und ggf. noch auf den Inhalt zugegriffen werden kann, würde ich dir empfehlen, nach einem delete noch einen nullptt an die Zeigervariable zuzuweisen. lrriy(. String». Vtktortn, Strukturen ^nd Zuger 116
RAM mit WoW-Freunden auf Anfrage Okay, jetzt nimm dir Zeit, die Messlatte in dem Praxisabschnitt ist recht hoch gelegt. Wenn du nicht drüber kommst, ist es aber auch kein Weltuntergang, weil C++ hier im Grunde einfachere Mittel an Bord hat. mit denen du das Beispiel realisieren kannst. Das verrate ich dir aber erst am Ende des Kapitels, wenn du daran verzweifelt bist. Okay, guck dir folgende Struktur an ... struct WoW_t { string klasse; string nickname; unsigned int freund_seit; WoW_t *next; >; Das ist eigentlich ganz einfach. Wie du Ja weißt, sind Zeiger sehr streng typisiert und können nur auf Adressen vom selben Typ zeigen. In dem Fall verweist dieser Zeiger quasi auf die Adresse einer weiteren Struktur vom selben Typ. Richtig, da wir allerdings nicht wissen können, wie viele solcher Strukturen in unsere Datenbank kommen, musst du das Ganze A^oA^_^ A^oA^-Z* A^öaZ-Z1 AAewtiftdefhAngen eiiuelner Knalen vb<r flnr Liitr 105 SCCNS
Hierzu jetzt der Codeausschnitt, mit dem du dynamisch unzählige neue Elemente ans Ende der Liste hängen kannst **-l- Freund hinzufügen\n,i; *2 "-2- Freunde ausgeben\n"; *2 **-3- BeendenAtV’; "Deine Wahl: “2 do while Schleife wird ein Menu realisiert. Dazu muss ja wohl rWrhts mehr geschrieben werden *1 H^er stehen die nötigen Zeiger für unsere verkettete Liste. Der Zeiger an fang verweist immer auf den Anfang der Liste. Der hilfsZcigcr durchläuft d«e Liste, und der Zeiger freund wird für die dynamische Reservietung eines neuen Objektes verwendet. •« Hier beginnt d« tintigen elittt rwiren Elementet .in d.ii Ende der verketteten Uster int wähl; WoW_t *anfang = nullptr» *hilfsZeiger, *freund; *1 do{ -2 cout < cout < cout < cout < ein » wähl; *2 switch(wahl) *3 { case 1; " freund - new WoW_t; cout « "Klasse : ’r; *6 ein » freund->klasse; cout « "Name : *6 ein » frcund->nickname;1'6 cout « "Verbund : *6 ein » freund->freund_seit; *6 iffnnfang nullptr) *7 ( an fang freund; freund->next nullptr ) eise *9 < riai neue Element angefo<dert Um den Code kurz w halten, wurde auf eine Fehleruber- pridung i ! OL) verzichtet« was du natürlich unterlassen solltest. *3 Über SWitch wird dx Eingabe aus- gewertet und zum gewünschten AVenübefehl gesprungen. •7 Ist der Anfang de* Liste gleich 01 (anf ang“0Li. sind noch keine Elemente in der Liste vorhanden und der anfang bekommt gleich die Adresse des neu dynamisch erstellten Elementes. Wichtig ist auch, dass d?f Zeiger next .i jf das nächste Element urt gar nichts (0L> zeig!. ’S Hier werden die Daten für das dynamisch resetyrerte Stfukturelement über d en Strukturzeiger •> e»ngelesen. Mehr zum StruklurzeigL'r erfährst du mich dem totlng. *8 Es ist also schon m ndestem ein Element in der Liste vorhanden hilfsZeiger » anfang; *9 while(hllfsZeiger->next ! nullptr} *9 t hilfsZcigcr - hilfsZcigcr->ncxt; ’9 } hilfsZeiger->next = freund; *11 freund->nexc nullptr; *10 } break; 2: cout « "Deine WoW-Freunde\n\n"; hilfsZcigcr - anfang; P12 whilefhilfsZelger ! nullptr) *12 < *9 Hier durchlaufen wir die Liste vom nfong bk zum lehrten Element. B ‘10 Hurra, wir fügen das neue Element am Ende de« tote hinzu. Der next- Ze»ger muss natürlich auch hn!r wieder auf nichts ze.gcn? "11 Oer zweite Atcnübefehl beginnt hier und lautet: Alle Elemente der tote nach- einander ausgeben >_____~~ B (Code bearbeiten) Besser wäre es. hier gleich noch einen extra Zeiger zu ver- wenden, der immer auf das Ende der Liste zeigt. In der Praxis ist diese Codestelle auch die Position, an der du die Daten mithilfe von Oberprüfun- gen sortiert dazwischen ein- fugen konntest Allerdings brauchtest du hierfür einen zweiten Hilfszeiger. *12 in einer Schleife werden die einzelnen WoW_t-Daten vom Anfang der Liste , durchlaufen und ausgegeben -1 । Strukturen u-nd Ztl9»r H7
cout « "Klasse : cout « "\nNatne cout « "\nVerbund hiIfsZcigcr } break; > }whfle(wahl ! * « hilfsZeiger->nlcknaKse; ” « hilfsZeiger->freund_seit; hilfsZeiger->ncxt; ’12 In einer Schleife werden <l t‘ cirwc nen WoW t- Daten vom Anfang der Ufte durchlaufen und ausgegeben. MS Ittetij] In C++ werden solche verketteten Listen natürlich nicht mehr mit einem solchen Overhead programmiert. Die intensive direkte Verwendung der Zeiger hat in der Vergangenheit zu vielen Fehlern geführt. Wie schon die Klassen vector und string für Ze-Arrays und Ze-Strings findest du in der Stan- dardbibliothek mit der Klasse forward_list eine viiieeel bessere und einfachere Alternative! Wenn du außerdem wirk- lich vorhast, verkettete listen manuell zu programmieren, dann aber bitte, wie es sich für eine OOP-Sprache gehört, über Klassen (class). Die Struktur- zeigen^ Bei dem Zugriff auf die Mitglieder einer Struktur. die als Zeiger (bspw. WoW_t ^Zeiger) realisiert sind, könntest du theoretisch auch wieder den Punkte- operator verwenden. Da es sich allerdings um einen Zeiger handelt, musst du für den indirekten Zugriff auch hier den Indireklionsoperator (das Sternchen *) angeben. Dies könntest du mit (^Struktur ) .mitglied realisieren. Zum Glück haben sich die Entwickler der Programmiersprache unserer erbarmt und den -> - Operator als Alternative eingeflihrt. Somit ist der Zugriff über einen Strukturzciger mit struktur->mitglied identisch zu (*struktur) »mitglied. Der Zugriff mit dem ->-Operator ist wesentlich komfortabler und auch sicherer zu verwenden. INilij] Nicht nur bei dynamischen Datenstrukturen wird der Zugriff auf die Elemente über den Strukturzeiger realisiert. Auch bei der Übergabe von Strukturen, bzw. genauer der Anfangsadresse der Struktur, an und in Funktionen, wird der Strukturzeiger verwen- det. Auch später bei den Klassen wirst du noch häufig mithilfe des -> auf die einzelnen Elemente der Klasse zugreifen. 188 SCCmS
RAM Unleashed lösen. Sei kein „Chicken". McSchrodinger, Das packst du schon. Zugegeben, dieser Abschnitt hat es in sich und dürfte dir nicht mehr soviel Spaß gemacht haben. Aber irgendwie muss ich dir Ja die dynamische Speicherreservierung erklären. Und da kann ein Museumsbesuch nicht schaden. Keine Sorge, nach diesem Abschnitt geht es mithilfe des Fluxkompensators wieder„Zurück in die Zukunft”, um den Schmelterlingseffekt möglichst gering zu hallen. (Notiz) An dieser Stelle noch eine wichtiger Hinweis für dich! Du solltest wissen, dass |5chwitfige AulgiJbt] Erwehre unsere dynamische Struktur Wow_t um einen weite* Menueintrag, mit dem du einzelne Strukturelemente aus der Liste mit delete entfernen kannst. Als Suchkriterium soll der ein "delete nullptr;H (bzw delete 0;) keine Probleme verursacht!!! Aus die- sem Grund solltest du immer eine Zeiger- variable gleich mit dem nullpcr bei der Deklaration initialisieren. Es hat sich auch bewährt, nach einem delete auch noch zusätzlich nochmals den nullptr an die bereits gelöschte Zeigervariable zuzuweisen. nickname verwendet werden. Okay, ich helfe dir ein bisschen Du musst auf jeden Fall zunächst überprüfen, ob das gesuchte Element gleich der Anfang ist. Ist dies der Fall, kannst du das Element mit delete zerstören. Vorher musst du aberden anfang Zeiger auf das nächste Element setzen, weil du sonst den Anfang und somit ganze Liste verlierst. Irrjy;. Strtng*- V»lctor»g, $t r uk C u'-«r. »n 0 hlqtr 189
Wenn cs nicht der Anfang ist, musst du die einzelnen Elemente der Liste durchlaufen. Hierbei solltest du immer einen Zeiger auf das vorherige und das aktuelle Element haben. Das ist wichtig, weil du nur so das gefundene Element .aushängcn* kannst, ohne dass die Kette reißt. Hierzu nun der Codeausschnitt *anfang • nullptr, ^hllfsZeiger, *freundp *hilfsZeigerZ nullptr: do{ cout « "-1- Freund hinzufügen\n,f; cout « ”-2- Freunde ausgeben^n”; cout <•- 3- Früund loschen\np : cout « ”-4- Beenden\n”; i cout « "Deine Wahl: "; ein » wähl; switch(wahl) < case 1: // alles wie gehabt • • • break; case 2: // alles wie gehabt • ♦ ♦ break; *1 Piewi weiteren Hilfszeiger • 1 benötigen wir zum Entfernen eines Elementes aus der Erste. welches sich nicht am Anfang befindet *2 Miet steht ctotrvg. '5 Das erste Element ist gleich des gesuchte Efaineftt? Zunichtt 3 Hie. geht * hilf sZeiger auf den Suche ruch dem anfang D« Ze^g« anfang 2U töMhendM aekorr mt dann die Adresse Element in der d“ n*fhitcn ««"«*» <**« H ™ch .nichts" («*0U sein kann). Jetzt hast d einen „rnufn" Anfang. fc4 Die gesuchte Person wird von Tastatur eingclesem^F string naae; *4 cout « ”Naae der zu löschenden Person: ein » namc; *4 /? if( anfang •• nullptr ) break; if( anfang->nickname == naie) y { ' hilfsZeiger anfang; *5 anfang = anfang->next; *5^r delete hilfsZeiger; *6 hllfsZeiger nullptr; eise *7 < hllfsZeiger anfang; ‘8 hllfsZeigerZ anfang->next; *8 while(hilfsZeiger2 I nullptr) ( '6 Jetzt kannst du das ehemals erste Element, auf das hllfsZeiger T noch verweist, rod delete g Htachen z *T fet nicht te " das «rite Element in der Liste, welches gelöscht t_ . .i ’ 8 Wir benötigen werden muss. Aw suchen J3 immer einen Zeiger wir danach . u uuf da*. Vorgänger- element t.nd einen auf du aktuelle 'S Dio Schleife wird komplett durchlaufen bis zum bitteren Ende .. 180 SCCMS
Das Element aushängen und löschen Irrjyj. Strings» V»ktor»ni Struktur»^ und Z»ag«-i
if(hilf$Zelger2->nlcknacse name) *10 < hilfsZelger->next • hilfsZciger2->next; *10 delete hilfsZelger2; *10 break; II Schleife abbrechen hilfsZeiger - hilfsZelgerZ; *11 " _ hilfsZcigcrZ hllfsZcigcr2->ncxt; '11 *11 fcicmcntc wurde*) «n der Schleife noch nkht gefunden. jIm> bekomnt hilfsZeiger die Adresse des iuvot überprüften Elementes, welches nicht das gesuchte wir und der Zeiger hilfsZeigerZ bekommt die Adresse des nächsten Elementes in der Liste. } } break; > }while(wahl ! 4); delete *-ej^z-sC€A 4^SS äc ^4 Speicherpfle^e ist gtnäusc wichtig wie Zahnpflege* Für e So, nachdem du dich wirklich bemüht hast bei diesem Thema und jetzt eine eigene WoW-Freundesliste-Anwendung geschrieben hast, ist es an der Zeit, dass du auch etwas für deine monat- lichen Gebühren bei Blizzard Entertain- ment bekommst und deine Fähigkeiten erweiterst äher *10 Wenn der gesuchte N.irr.r gefunden wurde, bekommt das Strukturmitgbcd next des Zeigers hilfsZeiger die Adresse vom Sttukturmitgiied next des Zeigers hilfsZeigerZ. Damit hast du das zu löschende Element .lusgeh.lngt und kannst CS mit delete loschen Nun ja, zum einen steht dir nicht unend- lich Speicher zur Verfügung. Bei einem dauerhaft laufenden Programm wie einer Server-Anwendung, wird der Spei- cher immer voller und das System auf Dauer immer langsamer (als Windows Anwender ist man das ja gewohnt). Und zum anderen werden hiermit quasi Löcher in den Speicher geschossen, so dass es dir im schlimmsten Fall passieren kann, dass du trotz ausreichend vorhandenem Spei- eher nicht mehr genügend zusammen- hängenden Speicher bekommst und die Speicheranforderung fehlschlägt. 192 upuii sccms
SIEBEN- Funktionen Funktionen, das Ende von Copy & Paste... Da Schrödinger In seinen alten B ASiC-Programmen auch Berechnungen für das Volumen der Schuhkartons Implementiert hat. plant er. die natürlich auch künftig In seinen neuen Programmen einzubauen. Allerdings würde er diese Berechnungen gerne mehrmals verwenden. .Schön blöd, wer hier seinen Code Immer wieder neu schreibt“, denkt Schrödinger und macht es sich mit Copy & Paste einfacher. Hier muss Ihm geholfen und gezeigt werden, wie er solche Codestellen als Funktionen implementiert, die er dann über eine Headerdatei (oder gar als Bibliothek) jederzeit in seinen Programmen verwenden kann.
Mitternacht, und cs ist Zeit für die Geister- stunde ... Also, nichts für schwache Nerven! In diesem Abschnitt wollen wir aber keine Geisler rufen, sondern eher Dinge beschrei- ben, die dir helfen, dass dir die Programmie- rung nicht auf den Geist geht. Und zwar geht es um die Zerlegung von Arbeiten in Teilaufgaben. Bislang bist du hergegangen und hast für eine Be- rechnung (oder Ähnliches) alles in die main Funktion geschrieben. In der Praxis wird aber genau so nicht programmiert! Hier zerlegt man in der Regel die Arbeiten oder Teilaufgaben in einzelne Funktionen (und gar Datei- en). Das Tolle an einer solchen Funktion Ist: Wenn du diese einmal definiert hast, kannst du sie immer wieder anhand des Namens aufrufen und verwenden. *1 Hier kannst du deiner Funktion em optional« Attribut vergeben (Hintergrundinfa) Gleiches gilt übrigens später auch für die Klassen. Nur \ dass hierbei keine einfachen Funktionen, sondern sogenai Elementfunktionen verwendet werden (= zu einer Klasse gehörende Funktionen), Die möglichen Elemente einer solchen Funktion lauten: (Attribut'1] Rückgabctyp*2 Funktiortsn«»e’3( Damit legst du fest, welchen Datentyp deine Funktion zunück- geben soll. Wenn du nichts aus deiner Funktion zuruckgeben willst, musst du stattdessen auch einfach VOid hinsdirciben. 194 sicacft // c e der Funktion ’6 *6 ... alle Befehle rein ailcs. was d»e Funktion so machen soJI *3 Ohne einen Kultigen Namen kann die Funktaxi auch nicht aufgenden werden. Hier kannst du alfes verwen- den, was bei den Vaiitbten erlaubt ist Aul bereits vorhandene Funktionrsnamen, die in enthalten sind, rolltest du abr* 4 Willst du deiner Funktion etwas zum Knabbern mitgeben. ist zwischen den runden Klammern der Platz dafür. Auch hier kannst du void reinschrciben, wenn du gar nichts an deine Funktion übeegeben willst
Meine Hausgeister An dieser Stelle muss Ich noch ein Thema aufgreifen, das nicht länger auf die lange Bank geschoben werden kann. Und zwar geht es wieder um eine Variable und deren Gültigkeitsbereich. Wenn du eine Variable innerhalb eines Anweisungsblocks deklarierst, dann hast du einen reinen Hausgeist - sprich: Diese Variable ist nur Innerhalb dieses Anweisungsblocks (ja. der mit den geschweiften Klammem {}) gültig! In der Regel hat man es meistens mit solchen lokalen Hausgeistern zu tun. Es Ist aber auch möglich, einen weltoftenen Geist Jenseits aller Anweisungsblöckc zu formulieren. Dieser globale Quälgeist kann dann überall verwendet werden. Du kannst nämlich durchaus mehrere Variablen mit demselben Namen verwenden, sofern diese alle in einem unterschiedlichen Gültigkeitsbereich liegen. Lass uns ein paar Geister beschwören: wB ’1 Das Ktl de* globale j j .4 Geist mit dem Namen string geist - "Bael", 1 ,Bdd.. DiewCcwtlU irfl gemimten Pro* int mainO «ramm sichtbar. "2 Heer hau du einen lofcalen Gertt mit dem Namen .Purson*. der >n der gesamten TOalllt ) Funktion sichtbar ist. tm ne mit dem X Hier vn der ab dieser Stelle im Code de« ade« tuH der xm nichsten gelegen { string geist "Purson” cout « geist « endl _ _ string geist • "Ipos*1 *3 Ein weitere« 1 Aftwefcirnpblock. cout << Seist <C endl • • > *3 cout « geist « endl rcturn 0; > *7 Hmr whrd wieder .PufHXT beschworen, weil dieser dec iokalsie GoKt ist. Der Geist Jpos" ist an dieser Ste«e gar nicht mehr bekannt. '6 JelJt wird natürlich der Gcl^M ipaV AuQjeuifen, weil dlrsq hier der nahen/ und t’okifci i$t Funk 195
Völlig richtig! Die lokalste Variable erhält den Zuschlag! Die globale Variable würde nur dann verwendet, wenn es keinen lokalen Bezug gäbe. Daher wird auch der Geist .Bad“ (= kann dich Unsichtbarkeit lehren; Imehr Geisler: hiip:/7www.gelsterarchiv.d<7]) Oberhaupt nicht beschwört. Und außerdem, nein zu den Geisteijägem, einfach nur ein Fan der Serie .Supernatural“, in der Sam und Dean Winchester (wieder mal} die Apokalypse ausgelöSl und am Ende doch noch vermieden haben. Doch, die gibt's! Entweder du rufst .Bacl" noch vorder Deklaration von .Purson“ am Anfang der md in Funktion auf, oder du verwendest den ZugriiTsopcrator (scope resolution operator) :: bei der letzten Beschwörung. Mit dem Zugriffsopcraior greifst du auf die globalen Quälgeister (global scope) zu. cout « :igelst « endl? 186 sicetw
Erster Kontakt zum Jenseits Um eine geistreiche Funktion zu erstellen, muss dein Programm mindestens den Funk- tionskopf kennen. Du musst wissen (solltest du wenigstens), dass dein Programm von .oben* nach .unten* durchgearbeitet wird (zumindest auf den Quellende bezogen). Da du in der Praxis eher selten alles in eine Quelldatei reinschreiben wirst, muss vor dem ersten Funktionsaufruf mindestens die Funktion deklariert worden sein! Die Defi- nition selbst kann irgendwo anders stehen. Mit einer Deklaration offenbarst du eine Funktion vor dem ersten Aufruf. Hier brauchst du nur den Funktlonskopf, abgeschlossen mit einem Semikolon, anzugeben. Für diese Informationen reichen neben dem Funktionsnatnen der Rückgabe-wert und die Parameter (geht auch ohne Bezeich- ner) aus. Auf den Anwcisungsblock kannst du bei einer Deklaration verzichten. Die Definition der Funktion kannst du an anderer Stelle im Code oder auch in einer anderen Datei schreiben. Der komplette Code der Funktion, also mitsamt dem Anwci- sungsblock und den Anweisungen, was die Funktion eben machen soll, wird als Definition bezeichnet. Hierzu ein Beispiel, das dir die Deklaration und Definition etwas deutlicher machen sollte und dir außerdem auch gleich die erste Funktion aufruft: linclude <lostreani> using namcspace std;
cout « "Vor der Beschwörungen"; gelstlchRuf () ; *2-— cout « "Nach der Beschwörung\n" rcturn 0; void geistlchRuf() ‘3 cout « "HuuuuhhhhVn1 Dat itf der Funktionsaufruf. 8xM einer Funktion müssen Imme* <hc runden Klammern mit angegeben werden, egal, ob du jetzt etwas in d»e Funktaxi übergibst oder nicht. Hattest du xurtx die Funktion nicht deklariert, würde sich der Compiler weigern, den Code ru übervrtzrn. weil ihm dann nichts über diw Funktion bekannt wäre Diese Funktion selbst macht nichts anderes, ah .Huuuuuhhhh“ auf dem Bildschirm auszugeben. Hier findest du die Definition der Funktion. Also die Funktion mit allem Drum und Dran. In der Praxis findest du eine $okhr Definition eher teilen im Haupt» Programm. Gewöhnlich lagert man den Code daru In eine separate Datei aus. l Kode bearbeiten] Bei einem solch einfachen Code hattest du auch auf die Deklaration verzichten können und gleich die Definition im Quelltext an die Stelle der Deklaration setzen können. Damit hättest du quasi die Deklaration und Definition in einem erledigt. Aber wie bereits erwähnt, bei umfangreichen Projekten ist das so nicht üblich. Das Programm bei der Ausführung 188 Ur.til SIC6EN
ilsterjäger Im Wohnzimmer Hey, lass doch jetzt den armen Kater in Ruhe! Zwar hast du die Funktionen kapiert, aber das ist kein Grund, gleich übermütig zu werden. Daher ist cs an der Zeit, zu überprüfen, ob du die ersten geistreichen Lektionen auch wirklich ver- standen hast. lEiofachr Auipbe] Mal sehen, ob du das Thema mit dem Gültigkeitsbereich von Variablen verstan- den hast. Im folgenden Listing wurde wieder dreimal der Bezeichner geist verwendet. Kannst du mir jetzt genau sagen, in welcher Reihenfolge welcher Geist in diesem Programm ausgegeben wird? Schau bitte nicht auf die Lösung! linclude <iostream> linclude <$tring:> using namespace std; string geist "Casper"; *1 void ausgibGeistI(); void ausgibGeistZ(); int tnain() ( string geist - "Dieter’'; ‘2 cout « :igelst « endl; ausgibGeist1(); ausgibGeisc2(); return 0; > void ausgibGeist1() { ’1 Gehi Nd.1 ktIonen 109
cout « gcist « endl; void ausgibGcist2() < string geist "Baltasar"; cout « geist « endl? } Und hier das Programm bei der Ausführung: (Achtuftf/ttorfichl) Dass bei der ersten Geist-Ausgabe nicht .Dieter*, sondern der globale -Casper* ausgegeben wurde, lag natürlich daran, dass der ZugrilTsoperator (scope resoluti- on operator) : : verwendet wurde. Bei der zweiten Ausgabe in der Funktion ausgibGeist 1 () wird abermals der globale .Casper“ ausgegeben, weil cs in diesem Bereich sonst keinen Geist gibt. Ohne den globalen »Casper* würde der Compiler gar die Übersetzung verwei- gern. Als letzter Geist wird in der Funkti- on ausgibGeist2 ( ) natürlich .Bal- thasar* ausgegeben, weil dieser innerhalb der Funktion angelegt wurde und somit der Funktion am nächsten liegt. Als Fazit dürftest du selbst einsehen, dass man von der Verwendung von globalen und gleichnamigen Variablen absehen sollte. Es empfiehlt sich, so etwas nur zu verwenden, wenn du kerne andere Wahl hast. Als Faustregel solltest du dir merken: So lokal wie nur möglich, und so global wie gerade so nötigt Im ersten Abschnitt zu den Funktionen will ich dich noch nicht überstrapazieren, daher kannst du eine Pause einlegen. Außerdem musst du noch ein neues Bett- laken kaufen, da du ja eines zerschnitten hast? 20t)
Blanke Funktionen ohne irgendwelche Parameter oder Rückgabewerte werden in der Praxis eher sel- ten gebraucht. Eine Funktion soll schließlich auch eine Schnittstelle mit dem Aufru- fer herstellen. In diesem Abschnitt wirst du zunächst die Obergabe von Parametern an Funktionen kennenlernen. AMt der Übergabe von Parametern ist natürlich nicht irgendwas gemeint, sondern die verschiedensten Typen, mit denen du bisher auch schon bekannt gemacht wurdest. Es gibt verschiedene Möglichkeiten, deinen Funktionen einen oder mehrere Parameter zu übergeben. Bei umfangreichen Datentypen ist die Möglichkeit, die Parame- ter als Kopie an die Funktion zu übergeben, deine schlechteste Wahl, weil In diesem Fall beim Aufruf der Funktion erst die Argumente an die Funktion kopiert werden müssen. Das bedeu- tet einen größeren Rechenaufwand, was besonders negativ ist, wenn die Funktion sehr häufig in kurzer Zeil aufgerufen wird. Natürlich bedeutet das auch. dass, wenn du Variable innerhalb der Funktion änderst, das keine Auswirkung auf die Original- variable des Aufrufers hat. da in der Funktion ja nur mit Kopien gearbeitet wird. JHxntcr<rundin«0| Um genau zu sein, ist bei diesem Call-by-Value-Verfahren alles doppelt im Speicher vorhanden, nur eben in unterschiedlichen Adressbereichen. Und der Aufrufer hat keinen Bezug mehr zu den Parametern, wie umgekehrt auch die Parameter der Funk- tion keinen Bezue mehr zum Aufrufer haben! Funktion«» 201
2 Hie, erfolgt dtr Funktionsaufruf mit den beiden Argumenten. In dem Fall werden die beiden Variablen ival und fval .in gabeö 1 und gabeD2 kopiert. Das Beispiel hierzu: *1 An der FunktiomdekUniliofl Cfkenntt du 5chf whon. diele Funktion zwei Parameter erwartet finmal ein int • - - und einmal ein float Bei der Oeklaxalnxi void huibuh( int» float ); *1 kannst du auch auf die Bezeichner ver- zichten. Diese Informationen braucht der Compiler vorerst noch nicht für int tnainO <He Obenetzupg. 4 int ival - I23; float fval - 123.123; hulbuhfival» fval); *2 *> findest du die Funirtkxnsdefwhboo. ' wo die Funktion m»t den beiden kopierten Parametern ausgeführt wird, im Beispiel werden einfach nur void huibuhf int gabeOl, float gabe02 ) ’3- die «erte ausgegeben. Änderungen an den Variablen haben keine Auswirkungen auf die Originalen ' ival und fval. cout « gabeOl « 1 1 << gabe02 « endls > (Achtungl Bei Funktionen mit Parametern ist es besonders wichtig, dass du die Reihenfolge (bei mehreren Parametern) der Argumente beim Funktionsaufruf einhältst und sie mit der formalen Parameterliste der Funktion übereinstimmt. Bei einer falschen Typubcrgabc findet eine implizite Typumwandtung statt. Das Thema hatten wir ja bereits* Die andere Alternative, um Daten an eine Funktion zu über- geben, ist mithilfe von Adressen. Und wenn von Adressen die Rede ist, sind die Zeiger nicht weit entfernt. Wenn du aus deinen Funktionsparametern einen Zeiger machst, brauchst du die Argumente nicht mehr als Kopie, sondern als Adresse zu Obergeben. Natürlich bedeutet das auch, dass sich Jetzt Jede Veränderung an den Variablen des Aulrufcrs in der Funktion direkt auf das Speicherobjekt auswirkt, weil Ja jetzt nur noch mit dem einen Speicherobjekt gearbeitet wird. (Achtung! Der Zugriff bei der Übergabe als Adresse hat jetzt natürlich auch zur Folge, dass ein (in-)direkter Zugriff auf das Speicher- objekt innerhalb der Funktion über das Sternchen (indirektronsoperator) erfolgen muss? 202 stcatH
Hier das (gekürzte) Gegenstück zum eben gezeigten Beispiel, nur jetzt mit der Übergabe als Adresse: _ void huibuh( inc% float* ); int ival - 123; float fval - 123.123; huibuh(iivalt fcfval); '2 *1 Die Funktipns- dcklaratw, nun mit Zedern ah I Parametern* *2 Be*m Funktions- aufruf muss jetzt natürlich eine Adresse übergeben werden. weshalb hier vor die VjnaWe ival und fval der Adress- operato* & gesetzt wurde. void huibuh( int *gabe01» float *gabe02 ) < cout « *gabe01 « 1 1 « *gabe02 « endl; *4 > . . Hier findest du die entsprechende Funkt»ons- del i nition. M Wichtig rtt es hierbei jetzt. wenn du auf den Inhalt des Speicher* Objektes zugreifen wulst. dass du den indlrektionsoperator dafür verwendest (auch wenn das recht umsundlkh er- scheint). Wenn du nicht mehr weißt, warum, blättere zu den Zeigern zurück! Watudich wurde em Zugriff wie *gabe0l«0; oder *gabe02"0 ♦ 0; auch bedeuten, dass (in-)direkt die Variable ival und fval auf 0 bzw. 0.0 gesetzt würden, da p jetzt mit deren Adressen \ I gearbeitet wird Der Meinung waren auch die C++-Köpfe und daher - Die Verwendung von Referenzen als Funktionsparameler bildet das Call-by-Reference-Verfahren mit den Zeigern nach - nur eben einfacher! Kurzer Exkurs zu den Referenzen Eine Referenz ist im Grunde ein ?\lias für ein Spcichcrobjekt. Bei der Geburt musst du eine Referenz bereits mit einem Speicherobjekt initialisieren. Die Referenz erkennst du bei der Deklaration daran, dass zwischen dem Typ und dein Bezeichner das & steht. Da musst du schon die Entwickler fragen! Wie dem auch seh das eine hat hier nichts mit dein anderen zu tun. Hier eine solche Referenz an der Front: int varlableBin • 123; int kreferenzBin « vari; rcferenfcBln « 345; ’2 *1 Die Referenz referenzBin ist Jetzt ein Alias für *2 Anita« wie beim Zeiger indirekt und umsUndlkti ithilfe dm rndiiektion^operjlo^ auf VHTlableBin zuzugreifen, ist de« Zugriff mithilfe der Referenz wesent- lieh einfacher Eben wie bc> gewöhnlichen Variablen Funktlö^tn 203
Zeiger und Referenzen sind über nicht dasselbe. Referenzen sind schließlich nur Aliasnamen für ein Speicher objeke, wobei du nur einmalig einen Wert zuweisen kannst. Zeiger hingegen können jederzeit Adressen von anderen Speicherobjekten aufnehmen. Weiter mit den Referenzen als Funktionsparametern _ die Definition der Funktion Die nur einmalige Zuweisung einer Referenz und vor allem ihre gegenüber Zeigern einfachere Verwendung macht Referenzen zu einer einfacheren Alternative als Funktionsparameter. Hierzu jetzt die Deklaration, der Aufruf und mit den Referenzen: Jfc FunktionsdeHnratiori jetzt als RMere-u’. void huibuhl int&, floate *2 Der Funktionsaufruf erfolgt genauso komfortabel wie bei der Übergabe einer Kopie, nur wird hier tatsächlich, dank der Referenzen, mit den Oiiginilwrrlen ival und fval geaibetel int ival float fval huibuh(ival, 123; 123.123; fval); ‘2 (*3 Oie Funktionsdefinition muss natürlich auch das Refcrcru^^ /riehen & zwilchen dem Typ und den Becerthnern tn den Parame- tern enthaften void huibuh( int ügabeOl, float 4cgabeO2 >1*3 < cout « gabeOl « ' " « gabe02 « endl; *4 } Nun Ja. wie bereits erwähnt, In der Praxis solltest du auf das alte Ze-Zeugs verzichten und stattdessen auf die Klassen vector und string zurückgreifen. Aber wenn du nicht umhinkommst. weil eine alte Schnittstelle nun mal nichts anderes will, gehe ich dir zulie- be auch kurz darauf ein. (Zettel) Referenzen als Funktionspara- meter bieten dir alle Vorzüge der Übergabe als Adresse (Call- by-Reference) und die einfache Verwendung von der Übergabe als Kopie (Call-by-Value). Idea- lerweise solltest du natürlich Referenzen mit dem &-Argu- ment anstatt Zeiger in C++ als Funktionsparameter verwen- den, Die Zeiger haben da eher schon etwas Ze-artiges an sich! 4 Auch die Verwendursg citolgt wesentlich komtortaNer. als dies mit den Zeigern möglich gewesen wäre. Und tratedem wird hier nur mit einem Ali.it vor ival und fval gearbeitet. Natürlich bedeutet es auch hier, dass Änderungen von gabcO 1 bzw gabc02 ebenso ival und fval . ändern würden. bin ganzes C'-Array bzw. einen C-String kannst du natürlich nicht als ganzes Stück an eine Funktion als Parame- ter übergeben. Aber das brauchst du ja auch gar nicht, weil hierfür die Anfangsad resse genügt. Letztendlich brauchst du also auch hier wieder nur einen Zeiger des Typs als Funkticmsparameter zu deklarieren. Zusätzlich solltest du allerdings auch die Anzahl der Elemente des Arrays bei den Parametern mit angeben. 204 sicetN
Und so wird's gemacht: *1 Der Funktkxiskopl bei der DekFdJdliO.n Def erste Parameter steht für die Anfang .idretse des C-Arr.iys, der zweite Parameter für d»c Anr.ihl der Elemente Elemente auf dem Bildschirm ausgeben. void huibuh( int*, int ); *1 int ivec(3) - { 3, 6, 9 }; huibuhfivcc, sizcof(ivcc)/sizeof(int)); ‘2 void huibuh( int *arr, int n ) { for(int i“0; Kn; ++i) { cout « arr[1] « endl; *4 > } "3 *2 Der Funktionsaufruf: Alternativ hattest du auch &ivec(0] für den ersten Parameter verwenden können. Aber das Thema hatten wir bereits. Mit sizcof lassen wir die Llnge des Arrays berechnen Bei C-Strin^s kannst du Statt- dessen die C-Standardfun ktion Strlen{) verwenden. Alles hier Beschriebene gilt natürlich auch für Strukturen und Klassen als Funktlonsparameter. Auch hierfür kannst du die Übergabe als Kopie, als Zeiger oder als Referenz durchführen. Also. Kopie ist oft die schlechteste Wahl, weil ja die kompletten Daten einer Struktur oder einer Klasse kopiert werden müssen. Warum man keinen Zeiger für call-by-Reference verwenden sollte, ist. weil man hierbei häufig die eigentliche gemeinte Semantik nicht erkennt. Bei einem Zeiger weiß man z.B. nicht genau, wem dieser gehört. Wer ist bspw. verantwortlich für das Aufräumen des Speichers? Bei einer Übergabe mit & kommt man z.B. gar nicht in Versuchung ein delete aufzurufen. Auch kann ein ^-Argument keine nullptr sein. Ein *-Argumcnt hingegen schon. Daher sollte ein call by.Reference in C++ mit einem & Argument gemacht werden. Ob du einen Zeiger oder eine Referenz verwenden sollst, hängt allerdings auch vom Anwendungszweck ab. Mal Ist die eine Methode die bessere mal die andere, ,x ' , y. / // ‘ z z" z* ’ z1// Nicht ganz, indirekt hast du mit vector und string bereits kleine Klassenerfahningcn. Hier ein Beispiel, wie du ein vector als Funktionsparameter verwenden kannst: ’2 Der Funktionsaufruf aß Fuöktiomfhrdre- ier ist dank der Referenz ebenfalls ein Kindcfiptcl linclude <veccor> void huibuht vcctor<int>i ); *1 vector<int> vec(3); hulbiih(vftc) 5 *2 *1 Am Funktiönikopf der DekLiration kannst du Khan erkennen, dass wu eine Referent verwenden, um das int-Array der Klasse vector aß Parameter zu reagieren. '4 Dank der Referenz <$t der Zugnff auf dir Ekmcntfunkbcmcn mrthiHe des Punktcapcra* tors einfach zu realisieren void huibuh( vector<lnt>l v ) *3 | *3 d^e Punktums- fortsixe t i-0; Kv.sizeO; ++1) ‘4 < cout « v[i] « endl; > } F unkt205
Unsere Gaben wurden angenommen... Okay, der ?\bschnitt im Büro mit den Parametern war ziemlich prall gefüllt, aber doch noch überschaubar Du solltest jetzt in der Lage sein, eine einfache Funktion mit Para- meter zu schreiben, Okay, ich nehme dich beim Wort IfirtfMh» Pitt ist ein Geisterjäger und packt seine Geister in Schuhkartons. Allerdings muss ein solcher Schuhkarton mindestens 3.500 Kubikzentimeter an Volumen haben. Schreib eine Funktion, welche das Volumen des Schuhkartons berechnet, und find her- aus, ob der Geist hineinpasst. Die Angaben Breite, Höhe und Länge fragst du noch im Hauptprogramm ab. Diese drei Werte gibst du als Referenz an die Funktion. Okay, einen Tipp gebe ich dir noch. Hier der Funktionskopf bzw. die l unktionsdeklaration dazu: void Schuhkarton( double!, double!, double! ); Okay, ich versuche es linclude <ioscream> linclude <vector> using namespace std; void Schuhkarton( double!, double!, double! ); 1 “2 Die Eingabe der einzelnen Werte für Breite, Höhe und lange ertolgt In der tfanktlon flL3 int mainO { double breite, hoehe, laenge| cout « "Breite (cm): ein » breite; '2 cout « "Höhe (cm): ein » hoehe; *2 cout « "Länge (cm): ein » laenge; 2 Schuhkarton(breite, hoehe, laenge); revurn OH "t '2 '1 Der Funktiomkopf wird dek oriert Ak Parameter wurden Referenden gefordert. 206 SICMw
void Schuhkarton( double &b, double &h, double il )l*4 ( cout « "VoInnen " « b*h*l ’5 « " Kubikzentimeter" « endl; ’4 Hier beginnt die FunklionsdeCntion. cout « "Der Geist passt if( (b*h*l) < 3500 ) *6 { cout « "nicht *| cout « "in den Schuhkarton\n"; > Und hier die andere geforderte Aufgabenstellung: Wenn das Volumen lb*h*l) Weiner ah 3.500 ist. pjwt der Geist n*cti! in den Schuhkarton, weshalb dann „nxhC *5 Hier wird das Volumen in Kubuk/enh- metern berechnet und doch gleich ausgegeben. md ausgegeben wird Kode bearbeiten) Im Sinne der Spaghetti-Code-Konvention könntest du den if-Vergleich auch mit der ? : -Abkürzung wie folgt realisieren: cout « (((b*h*l) < 3500)?"nicht "schon "); Das Programm bei der Ausführung: 6oer $ ./brutzel («>: 29 (»): 12.5 <cn): 11 i - 39Ä7.5 Kwbikzentiineter Boer S ./brutzel <em>: 27-5 (oi): 11 (oi): 10 i - 3025 Kubikzentimeter Dieter I Breite < Hohe । Lange i Volumen Der Geist passt in den Schubkarton Gho^ftrop Dieter I Breite । Hohe < Länge Volumen Der Geilt passt nicht in den Schuhkarton Dieter Boer S Q F unkt Ionen 207
Unendliche Stille Auch wenn wir jetzt noch keine Antwort aus dem Jenseits crhal len, muss ich dich loben. Du schlägst dich wirklich gut. Zugege- ben. die einzelnen Beispiele sind nicht unbedingt dazu geeignet, fette Geister zu jagen, aber für unsere Fälle reicht cs erst mal völlig aus. Wollen wir doch mal sehen, ob du mir bisher auch wirklich folgen konntest. als Kopie (Call-by- Value)/die Übergabe als Adresse via Zeiger (Call-by-Reference) und dann noch die komfortablere Version mit Referenzen (mit dem & zwischen Typ und Bezeichner), deren Anwendung sich so einfach wie die Übergabe als Kopie durch- führen lässt. tf irisch* Auiglb<] Im Grunde stehen dir drei Möglichkeiten zur Verfügung, die Argumente bei einem Funktionsaufruf an die Parameter der Funktion zu übergeben. Welche sind das? w. Völlig richtig! Hierbei ist natürlich noch anzumerken, dass bei der Übergabe als Kopie eine Änderung des Wertes inner halb einer Funktion keinen Einfluss auf den Wert des Aufrufenden hat, weil cs sich hierbei im Grunde uin zwei verschie- dene Werte mit unterschiedlichen Adressen handelt. Bei der Übergabe als Adresse (Zei- ger oder Referenz) hingegen wirkt sich jede Änderung natürlich auch auf den Wert des aulrufenden Argumentes aus, weil es sich ja schließlich um denselben Wert handelt. Wir hantieren hierbei ja wieder mit Adressen. Falls du nicht willst, dass bei der Übergabe einer Adresse via Zeiger oder Referenz die Werte in der Funktion verändert werden, kannst du diese mit dem Schlüsselwort const davor schützen: void func(const int& parameter); Durch das Schlüsselwort const kann jetzt parameter in der Funktion nicht mehr verändert werden. Das Schlüsselwort selbst lernst du noch kennen. 208 SICMw
O*e Utxrjsabc *1» Kopie urxl ab Achem sei hier noch- miH auf erner Serviette deutlicher darf-eitelit! Das ist natürlich hauptsächlich davon abhängig, was du machen möchtest Allerdings solltest du für einfache Basis typen die Obergabe by-Value vorziehen, wenn der Typ nicht geändert werden soll. Bei Typen mit mehr Speicherplatz solltest du die Übergabe by-Reference bevorzu- gen, weil der interne Rechenaufwand beim Kopieren sonst erheblicher ausfallen würde. ISthwitrit* Autgebel Oft ist nicht immer alles so eindeutig wie in den bisher gezeigten Beispielen. Im fol- genden Codeausschnitt wird zweimal ein Geist zum Geist_t-Array (vector) hin- zugefügt, und trotzdem wird keiner davon ausgegeben. Was haben die Geister gemacht, damit sie entkommen konnten? Kannst du die Geister wieder sichtbar machen? Hier der Codeausschnitt: struct Geist_t { string name; unsigned int alter; >; void Gef3tHinzufuegen(vector<Geist_t>, string, unsigned int); int main() < ‘1 Zwei Geisler werden hier über die Funktion GcistHinzufucgen() vector<Geist_t> geist; hiMugefögt... GeistHinjtufuegenfgelst, "Baltasar", 236}; *1 GeistHinzufuegenfgeist, "Nimrod'', 355); "1 for(size_t i«0; i<gei$t,$ize{); i**) *2 < cout « geist [i] .name « " '* « geist |i] .alter « endl; *2... und trotzdem wird keiner der beiden Genter in der Schleife aurgejje- ben? funkt lenen 209
> return 0» void GeistHinzufuegent vector<Geist_t> g, string n, unsigned int a ) m Gelst_t temp < n, a }; g. pushback(teap); *3 *3 In der Funklion werden dir r»ewen Genter kn Form der Struktur Geist_t ans Erxie gehängt. Das Problem ist denkbar klein, und vermutlich wirst du jetzt auch .Ahh' oder .Ohh" sagen. Im Beispiel wurde das Geist_t-Array (vector) als Kopie (!!!) an die Funktion übergeben. Das bedeutet, in der Funktion wird lediglich mit einer Kopie von geist gearbeitet. In der Funktion selbst werden zwar ordnungsgemäß die Daten an ein Geist_t-Array übergeben, aber eben nur an die lokale Kopie der Funktion. Nach Beenden der Funktion GeistHinzufügenf) wird auch die Kopie wieder verworfen. Du musst also lediglich für den ersten Funktionsparameter eine Referenz verwenden: void GeistHinzufuegen(vector<GeiBt_t>&, string» unsigned); void GeistHinzufuegent vector<Geist_t> &g, string n, unsigned int a ) { Celst^t tenp • { n, a }? g.push_back(tenp); ) [BHühMüflj/LfrSüflg] Zugegeben, das letzte Beispiel war ein wenig gemein, weil ich hier mit Absicht einen etwas komplexeren und großen Typen mit vector<Geist_t> verwendet habe, um dich von dem eigentlichen kleinen Problem abzulenken. Zur .geistigen" Ent- spannung empfehle ich dir, heute die Poltergeist-Trilogie anzu- sehen! 210 sicecw
Bisher war unsere Kommunikation mit der anderen Sehe immer einseitig. Wie bei einem antiken Telefon, bei dem die .Sprechmuschel fehlt. Alexander Graham Bell würde sich im Grabe timdrehen. Naja, Pferde fressen auch keinen Gurkensalat, und das Telefon wurde doch von einem Deutschen erfunden! Basta! Wenn du also eine Antwort von deiner Funk tion haben willst» musst du zum einen den Funktionskopf entsprechend anpassen und mitlcilcn, von welchem Typ die Amwon sein soll. Zum anderen muss bei der Funktionsdcfinition in der Funktion irgendwo auch mittels return die Antwort, natürlich vom passenden Typ des Fun kl Ions* kopfes, an den Aufrufer zurückgege- ben werden. Der Aufrufer selbst sollte natürlich auch noch den Rückgabewert irgendwie behandeln und diesen z. B. mit dem Zuweisungsoperator = an einen passenden Typ übergeben. _- . _ . _ . < . * Ein einfaches Beispiel hierzu: int derGeiscAncwortet( const inU -t AmhmktionskopIderOeMMaöMi kannst du sehr schön erkennen, dass diese Funktion einen int-Typ zurütkgibe und als Parameter ebenfalls einen Typ int erwartet. ‘2 Der Aufrufer weist nun diniert Rückgabewert der Variablen hui zu. Ide al erweise handelt es jkh Herbei auch um denselben Typ. der von der Funktion zuruck- gegeben wird. Ansonsten findet eine implizite Umwandlung statt int inainO < int buh = 333, hui; hui derGcistAntwortct(buh); 4d cout « "hui " « hui « endl return 0; > "3 H>er die entsprechende Funktions- defin tton. Das const beim Funktions- parameter zeigt an. dass val in der Funktion nicht verändert weiden darf Aber das nur so am Rande, hat nix mit der Rückgabe zu tun! ( return val+val; ’4 int derCeistAntwortct( const intfc val ) *3 M Mithilfe des return-Belehk wird die Funktion beendet und das Ergebnis (in dem Fall der Wert 666) des Ausdrucks val+val an den Aufruhr zurückgegeben. Den Ausdruck val+val kannst du aber auch (eben als Ausdruck) zwischen zwei Klammern letzen, wenn m passt (bspw. return (val+val) ;). Funktion*» 211
|H«ntrrxrundinM Den return-Befehl kannst du auf unterschiedliche Art und Weise verwenden. In der raain-Funktion bedeutet ein return, dass,das Programm beendet wird, egal mit welchem Rückgabe- wert. Bei Funktionen, die keinen Rückgabetyp (void) haben, kannst du mit einem leeren rcturn; die Funktion ohne einen Rückgabewert beenden. In dem Beispiel hättest du auch gleich mit cout « derGeis(Antwortet(buh) « endl; auf die Zuweisung und Zwischenspeicherung des Rückgabewertesan hui verzichten und sofort die Rückgabe' mit cout auf dem Bildschirm ausgeben können. Natürlich kannst du auch mit Hilfe von Zeigern nur die Anfangsadresse der Daten zurückgehen. Das könnte recht nützlich bei umfangreichen Daten sein, weil hierbei nicht das komplette Faket zurückgegeben werden muss, sondern nur die Anfangsadresse darauf. Trotzdem ist das nicht mehr sehr O*-Ig! Selbst wenn du ganze Objekt zurückgibst, kann der Compiler als Optimierung die Kopie entfernen. Gerade mit C++11 gibt es ja hier auch eine neue Move-Semantik (ich erzähle dir das später), womit es nicht mehr nötig ist. Zeiger als Rückgabewert zu verwenden. Da es aber auch noch antiken Code gibt, wo Ze-Zeugs mit Ze++ gemischt ist. soll das Thema der Vollständigkeit zuliebe hier erwähnt werden. Ein solcher theoretischer Funktionskopf sieht wie folgt aus: TYP* funktionsnane( parameter ); Aufgerufen wird diese Funktion dann so: TYP* zftigcr_auf; zeiger_auf funkcionsname( argumence); Der Zeiger zeiger_auf enthält nach dem Aufruf der Funktion jetzt die zurück' gegebene Adresse der Funktion funktionsname (). 212 sicecu
Nein, hat er nicht! Das Problem ist nämlich folgendes: Wenn du bspw. einen Zeiger von lokalen Daten einer Funktion an den Aufrufer zuruckgibst, ist diese lokale Adresse beim Beenden der Funktion nicht mehr auf dem Stack (da wo die Funktionen leben) gültig. Auch wenn du zunächst vielleicht mit den Werten Weiterarbeiten kannst, ist die Verwendung solcher Daten eine lickende Zeitbombe und absolut unzuverlässig. Sieh dir folgendes Beispiel an: vector<int>* Gniselstundefconst intfc n, const Inti val) { veccor<£nt> lokalerMlst(n); *1 forfint 1=0; i<n; ++i) { lokalerMisc.acd) val; *2 > return(llokalerMisc); *3 •2 AHcn Ek-mcntc werden mit dem Wert val initialisiert. *1 Miet legst du rin fok-ilrs int-Aruy (mithilfe der Klj$5C vector) m« q Elementen an. Das ist die tickende Zeit- bombe! *3 Hier wird die lokale Adresie von lokalerMist an den Aufrufen mittels retUHl zuruckge-ßebe-n vector<lnt> ^Adresse lokalerMist. - Gruselsvunde(5, 666); ‘4 lokalerMist 1-iClC^X *4 Sosietil der funktiqns.iufrut von Gruselstunde (> aus. Oer Rückgdbewen wifd natürlich einem zum Eunktionskopf passenden Rückgabewert zugewiesen. Ja. das siehst du völlig richtig- Mindestens haltbar bis: Funktion zu Ende, Die Werte, die du von Gruselstunde ( ) zuruck- bckomnisl. sind wirklich totaler Mist. wt*i Nun ja. es gibt hier schon Wege. Gammeldalen daraus zu machen, aber keiner davon Ist wirklich gesund für das Programm. Neben der schlechtesten Möglichkeit. eine glo- bale Variable zu verwenden, könntest du noch ... * ... einen Speicher mir new von der Speicherhalde anfordern, der zur gesamten Laufzeit des Programms zur Verfügung steht. Allerdings musst du dich hierbei auch wieder um die Freigabe des Speichers mit delete selbst kümmern. Daher würde ich dir als bessere Alternative gleich den neuen unique_ptr (C++11: zeig ich dir später) oder notfalls den alten auto_ptr (zeig ich dir auch später) empfehlen. ... eine Variable mit dein Schlüsselwort Static versehen. Dadurch wird der Typ nicht auf dem Stack, sondern auf dem Datensegment gespeichert. Funktionen 213
Auch Referenzen kannst du als Rückgabewert einer Funktion deklarieren. Spater, wenn cs um obskure Sachen wie die Operatorüberladung geht, wirst auch du den Zauber der Referenzen verstehen. Im Augenblick stellt eine solche Referenz als Rückgabewert für dich eher ein potenzielles Sicherheitsrisiko dar. Hier ein solches Problem: int& Horrorstunde( int buzz, int bazz < if( buzz > bazz ) { _____________________________ re turn buzz; *2 return bazz; *2 *1 Die Funktion Horrorstundef) gibt cinr int-Refaffffiz zurück *2 Die Funktion würde gerne den größeren der beiden Integer zurückgeben D.w Problem iit nur, dass buzz und bazz hier eben wieder lokale Variable (Calhby-Vatue) sind, und da ist nun mal <te Haltbarkcltsdatum zum Ende der Funktion abgelaufen An sich ist deine Idee gar nicht mal so schlecht. Allerdings funktioniert das dann nur, wenn du deine Funktion wie folgt aufrufen wurdest: Int Jason I23w caeyers 345; int ratsch = Horrorstundetjason, treyera); Bei konstanten Werten hingegen wird sich dein Compiler wt-igem, weiterzumachen. weil es für ein konstantes Argument keine Adresse gibt, Diesen Funktionsaufruf würde der Compiler somit kopfschüttelnd ablehnen: int ratsch - Horrorstundet 123, 345); ff !!! Fehler Ml 214 Upit<i SlCfttm
Die Stille Ist zerrissen So, da du jetzt mit dem Datenaustausch von Funktionen in beide Richtungen vertraut bist, werden deine Programme eine immer athletischere Form bekommen und wesentlich kom- munikativer sein. Mal schauen, ob du mit der neu hinzugewonnenen Schönheit auch etwas Stimmung machen kannst? Hier wollen wir jetzt einen Rückgabe-wert haben Oie Parameter sollten außerdem als Kopie übergeben werden, damit es nicht zu Probäemen mit konstanten Argumenten kommt. Also weg mit den Referenzen* (Einfache Aulgabe) Sicherlich erinnerst du dich noch an die Funktion Schuhkartonf ), mit der wir einen Geist fangen wollten? im Grun- de war diese Funktion ein wahres Kon- strukt der Hässlichkeit. Daher lautet deine Aufgabe jetzt, die Funktion umzuschreiben und das Volumen des Schuhkartons als Rückgabewert auszu- geben. Zusätzlich soll dir eine extra Funktion Geisterfalle(> als Booleschen Wert zurückgeben, ob der Geist in die Schuhkartonfalle passt oder nicht. Damit du dich beim Zurückbläl- tern nicht am Papier schneidest, findest du diese -ugly" Funktion mit ein paar Tipps hier abgedruckt, an der du nun dein Skalpell ansetzen solltest. void Schuhkarton( ( double 4bt double 4h, double 41 ) cout « "Volleren » " « b*h*l " Kubikzentimeter" « endl "Der Geist Diese Berechnung tollte nun am dei Funktion rutück» rereben werden passt cout « lf( (b*h*l) < 3500 { cout « "nicht " cout « "in den Schuhkarton\n"; ’S Das wollen vr-r Irt dieser Funkvon nicht mehi sehen und soll in eirM? extra Funktion Geisterfalle () abgelagert werden.
Hierzu ein Lösungsvorschlag: *1 Die neuen Funk!HMiiköplc bei der Deklaration zeigen whon. # ine lüde <iostreacn> wat dlcic ah Argumente erwarten uslng namespace std; und wat wruckgegeben wind. double Schuhkarton( double, double, double ); bool Geister fallet double ); *1 int mainO hoehe, (cm): double breite, cout << “Breite ein » breite; cout « "Höhe ein » hoehe; cout « "Länge ein » laenge; double volumen cout « "Volumen: if( Geisterfalle(volumen) cout > eise { cout > return > laenge; ♦I « Jetzt ßibt um die Funktion Schuh- karton ( ) das berechnctr Volumen (cm) (cm) *3 liefert die Funktion Gcisterfalle() den Wen true zurück. pa«t der Geilt in den Schuhkarton, bei eise nicht Schuhkartonfbreitef hoehe» laenge) " « volumen « true) Der Geist passt in die Falle\n”j 2 Kubikzentimeter\nn; { ’4 Bri der Funktionv- d-cfi nrtto-fi voji Schuhkar- ton( ) wurde erheblich abgespeckt. Die Funktion macht jetzt nichts anderes mehr. als dei doublC-Wert von den Parametern b h und 1 zu rndlipfiheren und an den Aulrufer zurückzugebcn. Der Geist passt nicht in die Falle\n 0 double Schuhkarton( double b, double hv double 1 ) retum (b*h*l); > bool Geisterfalle(double vol) { retum (vol > 3500) Hier wird t»n Boolescher Wed (true Ode/ falseI zunxtge true vo’d an den Aufrufe« zvrüikgegebe*i, wenn der übergebene ParamrUu vol -m re turn Ausdruck größer oder gleich 3 .500 rtt Trift! dien nicht zu, gibt der retum Aufdruck false ru*uck 216 caoitel SICttH
Hausgeister zurückgeben Wie bereits erwähnt, Ist es Immer eine heikle Sache, solche Dinger an den Aufrufer zuriiekzugeben. Wenn du also etwas Lokales aus einer Funktion zurückgeben willst, musst du entweder den Speicher von der Speicherhalde (Heap) oder von dem Datensegment ver- wenden. Der Wohnort der Funktionen, der Stack, ist dafür nicht geeignet, weil dieser am Ende wieder zerstört wird. r 1 Der Funktionskopf gibt uns Auskunft darüber. dass eine Adresse vorn Typ int iurück- gegebert wird. Eine solche Rückgabe von der Speicherhalde kannst du mittels new realisieren: nt* Grusel stunde (const int n, const int val) *1 < jetzt den Speicher »ür n housgeist(cr) n de/ Speicherhalde holst, Ibt die Haltbarkeit auch über die Funktion hinaus bestehen. t *hausgeist new int for(lnt 1*0; i<n; ♦*!) ( Hausgeist[i] a val; } return(hausgeist); In); *2 *3 Wenn du jeden hausgeist mit dem Wed val inMialisiert , hast. wird die Antangs- .idrew des dynarnneben Ze-Array» zurück* i gegeben. Den Rückgabe- wert <die Anfangs dres tfi s Funktionsaufrufes müssen wir natürlich auch mithilfe eines Zeigers sichern. *beschwoeren - Gruselstunde(5» 666); "4 1^/ftS i Schlimm ist diese Verwendung im Grunde nicht. Das Problem ist an sich eigentlich die Freigabe des Speichers. Da dich ein solcher Programmicrstil praktisch zwingt, den Speicher an einer anderen Stelle des Programms auch wieder frei zugeben. passiert es schnell, dass du das vergisst und eine Leiche auf der Halde ubrig bleibt (Memory Leak). Außerdem gibt es bessere Alternati- ven zu einem solchen Ze Zeugs. Funktionen 217
Jetzt bist du ein Medium Da du nun dir komplette Kommunikation mit den Funktionen kennst, bist du ein Medium. Als Repräsentant und Übermittleran Informationen kennst du jetzt die grundlegenden Techniken, um auf astraler Ebene zu interagieren. |Ei*ritht Aulglbt] Lokale Variable von Funktionen haben eine endliche Haltbarkeit und sind daher nicht als Rückgabewert einer Funktion geeignet. Wie lange sind diese Variablen gültig? Der Kandidat hat 100 Punkte. Noch eine Aufgabe: fSthwierifcr Anf^ibe] Im Grunde ist es ja nicht direkt möglich, zwei Werte gleichzeitig aus einer Funktion zurückzugeben, Mit einem einfachen Trick gelingt es aber doch. Schreib eine Funktion, welche zwei int-Parameter erhält und aus diesen beiden Parametern jeweils einmal die Summe und einmal die Multiplikation zurückgibt. Gib die Ergebnisse beider Berechnungen mit einem Ruckgabewert zurück. Die typische KMnniumkitioH mit einer Funktion mit Schfadinger ils Medium dir wischen ... iWfltieren/Übrnl Okay, ein Tipp: Du kannst entweder einfach einen int‘Zeiger auf ein dyna- misches Array mit zwei Werten zurück’ geben, oder du verpackst die beiden Parameter in eine Struktur und gibst diese an den Aufrufer zurück. Der Vorteil der Struktur ist natürlich, dass du unter- schiedliche Typen zurückgeben kannst. 218 ur.t.i stcetw
*1 die Struktur mit zwei Int-Weften für die Addition und lYiültiplikatHHl Hier ein Lösungsvorschlag: *3 Speicher von der SpekhethaMe für eine neue Struktur von Typ Paar t An'ordern der funktions- lufruf struct Paar_t { *1 int multi; *1 *2<he Int addil *1 Funktion». deiimtion >i 1 Paar _t* AddiMultiUnc vl, int v2) *2 < ßaar_t* einPaar - new Paart; *3 einPaar->nulti vl*v2; *4 einPaar->addi vl+v2; '4 return ein?aart«V-.-* »J...„urtddl(Ml } zurück damit an den Aufrufen Paar_t *polter - AddlHulti(20, 10); *6 *4 einmal die Multiplikation und einmal die Addition im entsprechenden StcuWurmit- glied speichern INotü) In der C++-Praxis musst du natürlich keinen eigenen Typen Paar_t definieren, weil dir C++ für solche Zwecke mit std: :pair<> schon etwas fertiges zur Verfügung stellt. cout « polter->aiulti « " ” « polter->addl « endl; Die Lösung macht zwar, was sie soll, aber räumt nicht hinter sich auf Aber du weißt ja, dass es nicht unbedingt von Vorteil isi. wenn du einen Speicher von der Halde anfor- derst und diesen aus einer Funktion zurückgibst. Die Geschichte hat gelehrt, dass gerne rde. den Speicher wieder freizugeben, so dass sich die Leichen berge nur verge so st hcn! |fäntuhf Aufjabcl Verbessere die Funktion AddiHultif) nochmals, so dass du keinen Speicher mehr von der Halde anfordern musst (aber auch nicht das Schlüsselwort static verwendest [für den Fall, dass du damit vertraut bist]). Die verbesserte Lösung void AddiMuici<Paar_c& einPaar, int vl, int v2) ’1 •1 Der verbesserte Fvnktionskopf. Hierbei einPaar.multi vl*v2; ‘2 einPaar.addi - vl+v2; *2 ‘3 Auch der Funktiönuufrul wird kein Wort aus- der Funktion wiückgcge- ben. StAttdeiien wurde die Struktur Ah Referenz im eriten Parameter verwendet. Ein Zeiger wäre auch gegangen .., • • • gestaltet sich jetzt wesentlich Paar_t polter; leicfiier. AddiHultKpoltcr» 20, 10}? *3 cout « polter.emlcl « *' *' « polter.addi « endl; *2... Aber die Referenzen lauen skh emücher benutzen All die Zeiger. H ier |BHötaurig/L6futtg] Ak offiziell anerkanntes C++-Medium würde ich dir jetzt empfehlen, zu meditieren und dich für höhere Aufgaben bereit zu machen. werden die Ergcbnme in den StruHurvafMth- len gespeichert F unk t lon«-n 219
Ich muss dich nochmals stören, lieber Guru. Auf deinem Weg zum Nirwana haben wir noch ein paar Dinge nachzu- holen. Ein paar Spezialitäten haben wir hier noch nicht behandelt. Rufst du eine Funktion ohne Argumente auf, obwohl der Prototyp der Funktion danach schreit, wird dein Compiler die Arbeit niederlegen. Du kannst aber für einen solchen Fall auch mit Slandardparametcrn (Default-Parametern) Vorsorgen. Hier ein solcher Prototyp: void washadDu( int aWert = 88884444 ); Da die Funktion washadDul ) nun mit dem Standardwert 88884444 vor- belegt wurde, bedeutet das. dass du diese Funktion ohne Argumente aufrufen könntest. In dem Fall wird eben der Stan- dardwert 88884444 verwendet. Ansons len, wenn du die Funktion bspw. mit washadDu(44448888) aufrufen würdest, würde, wie für Funktionen üblich, der übergebene Wen 44448888 verwendet. Wie für eine Deklaration üblich, kannst du auch auf den Bezeich- ner (hier: aWert) verzichten. iztu«n Gleiches geht natürlich auch mit mehreren Parametern. Allerdings musst du dann wissen, dass die Zuordnung der Argumente beim Aufruf von links nach rechts erfolgt. Für den Prototyp bedeutet das, wenn ein Parameter keinen Standardwert hat, kann der nächste Parameter (links davon) auch keinen Standard- parameter mehr enthalten. Somit sind folgende Prototypen erlaubt: void pft(int pl=23, int p2=34); void pft(int pl, int p2-34); void pft(int pl, int p2); IEin Fehler hingegen wäre: void pft(int pl=23, int p2); 220 uc.tn sicctN
Dte Übergabe der Parameter erfolgt v^n linkt ruch rrcMv Argument 1 gehört zu Parameter!, Argument? Ju Parameter? und Argument J |y Parameter?. UbergitHt dw bspw. nur ein Argumenl, werball Parameterl den Zuschlag wnd Parameter? und Parameter) mussten dann einen Slandutfparameter besitzen, damit der Compiler das mümacM. :erschledliche Typen Bisher bist du davon ausgegangen. dass alles in einem Gültigkeitsbereich einzigartig sein sollte. Zumindest habe ich das ja so gesagt. Bei Funklionsnamen gibt cs da eine Ausnahme. Du kannst Funktionen im selben Gültigkeitsbereich mit dem gleichen Namen verwenden. Einzig die Parameter müssen sich hierbei vom Typ und/oder der Anzahl her unterscheiden! So etwas nennt sich dann Funktionsüberladung: int puupflnt brot); int puupfint brotl, int brot2); double puup(double brotli double puupfdouble brotl, double brotZ); Jetzt hast du hier vier Funktionen mit demselben Namen. Der Compiler identifiziert die gleichnamigen Funktionen anhand ihrer Signatur (also der Parameter). Auch wenn hier nur die Deklaration abgedruckt wurde, sollte klar sein, dass du für jede Deklaration auch eine extra Definition erstellen musst! Wenn du eine Funktion aufrufst. wird dafür ein Stack-Rahmen angelegt, innerhalb dessen sich die Funktion austoben kann. Der Stack ist eine gesonderte Einheit (wie bspw. die Speicherhalde) auf dem System, welche von einer Funktion verwendet wird, in solch einem Stack wird zunächst die Rücksprungadresse zur Navigation gelegt, damit das Programm am Ende der Funktion weiß, wo es zurück geht. Auch für den Funktionen 221
Rftckgabetyp und die Parameter wird bei Bedarf ein Speicher auf dem Stack angelegt. Während der Ausführung kann dann noch Platz für die lokalen Variable vom Stack beschafft werden. In engen Schleifen kann es sein, dass du keinen solchen Wirbel auf dem Stack erzeugen willst. Der ganze Zauber auf dem Stack verplempert ja auch Rechenzcit. welche du viel- leicht an manchen Stellen vermeiden willst. Für solche Zwecke kannst du das Schlüsselwort inline vor die Funktion stellen: Inline double stackwlrbelwind(double, double» double); Mit dem Schlüssel wen inline schlägst du dem Compiler vor. dass dieser den Code der Funktion nicht als aufrufbare Funktion in einen Maschinencode übersetzt, sondern dass der Code an Ort und Stelle verwendet werden soll, dort, wo diese Funktion aufge- rufen wird. inline «t n :h>. mil d-rn Rollen iu tun. Wind«en schlagt (km Compikf vor, nnr Funktion An Ort und Stelle «Mnzufcauen, anstatt den umtlxndlichrn Weg ubr< den Stack zu 222 sicctH
iHinUrfrundinfo) Wie gesagt, mit dem Rollerblades-Schüsselwort machst du dem Compiler nur den Vorschlag, den Code an Ort und Stelle des Funktionsaufrufes einzufugen. Letztendlich entscheidet der ompiler selbst, ob er auf dich hört! Willst du dir 1OO9tig sicher sein, musst du den Code schon direkt an Ort und Stelle befehli- en. .anstatt einen Funktionsaufruf zu starten. (ZtU«l) In C++11 gibt es außerdem mit der Lambda-Funktion tatsächlich die Mög- lichkeit, eine Funktion (als Funktion*- Objekt) an Ort und Stelle zu definieren und auszuführen. Aber das wäre jetzt schon etwas zu früh und daher erkläre ich es dir später. main( ) die erste Funktion ist, die dein Programm ausführt, ist dir ja längst kannt. Ohne eine solche Funktion kannst du kein ausführbares Programm erstellen. Aber wie gelangt cs zum Ende? Hierzu eine kurze Liste, was deinem Programm alles ein Ende bereitet: “ Wird innerhalb der main-Funktlon return aufgerufen, wird die main Funktion und somit das Programm beendet. In Funktionen, die sich auf dein Stack austoben, bedeutet ein return nur einen Rücksprung zur Adresse an den Aufrufer. “ Mildem Aufruf der Funktion void exit(int n) ; aus dem Header <CStdlib> kannst du das Programm mit einem ganzzahligen Wert an Jeder beliebigen Stelle beenden. Allerdings kann bei dieser Funktion das Saubermachen von lokalen Variablen versäumt werden. ** Noch härter kannst du dein Programm an jeder Stelle mit der Funktion void abort ( ) ; aus <CStddlib> abschießen. Allerdings wird damit gar kein Müll mehr entsorgt, und was nach einem abort geschieht? Keine Ahnung, hängt von deinem System ab..,! Durch einen Ausnahmefehler (eng). Exceptlon), den du mit einer Ausnahme- behandlung abfangen kannst, gelingt ein Abbruch auch. Dazu kommt es noch! * Unerwartete Fehler, die vom Programmierer erstellt und dem Anwender produziert wurden, führen ebenfalls zum Ende. Meistens handelt cs sich um Bugs wie einen Speicherzugriffsfehler, die vom Programmierer behoben werden müssen. F wnktlontn 223
Jenseits von Eden Um mit den Funktionen würdig abzuschliclicn und um eine neue Ebene deiner Eigen- schaft als C*+-Guru beschreiten zu können, wellen wir ein paar der Spezialitäten noch- mals in Aktion sehen und erleben. Als erstes Beispiel die Standardparameter Jinclude <iostream> using namespace std; •1 Der zweite Parameter der Funktion einKreisen() erhält den Standard wert 10. wenn die Funktion ohne ern zweites Argument aufgerufen wird double cinKrciscnCdouble double • 1.0); Int main() < double flaeche a einKreisenf1.8); double volumen » einKreisenf1.8, 2.5); ’Z Hier wird nur ein Argument an die Funktion übergeben, so dass für das zweite Argument automatisch dr Stanrfardp.ir.i- meler mH dem Wert 1.0 verwendet wird. cout « "Kreisfläche cout « "Voluocn-Zylinder return 0; flaeche « volunicn « endl; endl; double elnKreIsen(double r ( recurn (r*r*3.1415*h)5 > double h) M ‘4 In der Funktiem- definition wird der Sl.indardpaMrneter nicht mehr.ingegeben •3 Hier übergeben wir zwei Argumente an die Funktion, woduich der Standardwert 1.0 nicht zum Emwttr kommt und als rwertrs Argument hier 2.5 verwendet wird. (Einfach» Aufgabe] Hm, eine einfache Frage: Kannst du dir denken, was diese Funktion macht, wenn du sie mit nur einem oder mit zwei Parametern auf rufst? f r*r*3.1415 ist doch die Berechnung für eine Kreisfläche, und wenn ich jetzt noch mal h rechne, sollte das Volumen für einen Zylinder berechnet werden. Mit einen Parameter wird die Kreisfläche (weil X*1.0=X) und mit zwei Parametern das Volumen eines Zylinders berechnet. Nichi schlecht. Schrödinger! Du kannst ja doch rechnen. 224 sictCM
ICod« bearbeiten) In die Funktion solltest du noch eine Fehlermeldung einbauen, falls für das zweite Argument irrtümlicherweise 0.0 an die Funktion übergeben würde, was ja überhaupt keinen Sinn machen würde, well dann auch 0.0 herauskäme. Sicherlich willst du jetzt auch noch ein Beispiel zur Funktionsuberladung sehen? Auf zum Überladen int bigmamafint, int); *1 double bigmama(double, double); *1 gering bigmama(const stringl, const stringi)| *1 ’1 Hier stehen drei Funktionen mit demselben Bezeichner, aber ufitcrschiedHchen Parametern (und Rcickßabcwerten) - eine klassische Funktion^* ubertMking int main() < cout cout cout return 0; > « bigtnatna (3.3, 2.2) « endl; « bignwraa (3, 2) « endl; « blgnaeia("aaa", "aab”) « int bigmamafint vall, int val2)|’5 < if(vall > va!2) { | return vall; } return va!2; 1 3 Das bnngt die int-vcrsKKi bigmatna () ins Spid. *2 Hiermit wird dir double-Vcftipn adgerufen, *4 Und das ruh die string Version auf den Plan. ndl; doxible bigmama(double vallt double val2) { iffvall > val2> { return vall; > return va!2; > string bigmama (const string &vall» const string &val2) iffvall > val2) { return vall; > return va!2; > •5 0>c Funklions- dehndronen aller drei überladenen bigmama ( ) • Funktionen Auch der Code iw bei alten drei Funktionen identisch, fs muts aber nicht so semi Funktionen 226
cj ^/^i^zCe Vereinfacht dargestellt, durchläuft der Compiler folgende Reihenfolge: 1 • Findet der Compiler eine Funktion mh passender Signatur (Parameter), wird diese auch verwendet, logisch! 2 • Der Compiler versucht eine Funktion zu finden, um eine integrale Promotion durchzuführen. 3 • Geht auch das nicht, sucht der Compiler nach einer Funktion, mit der sich eine Standard-TypuinWandlung durch- führen lässt. 4 . Jetzt gibt cs nur noch die Möglichkeit einer benutzer- definierten Umwandlung, oder der Compiler beschwert sich über deinen Code und gibt auf. INotll] Wenn du dir die Stichpunkte genau durchgelesen hast, durftest du auch zu dem Entschluss kommen, dass du die int-bigmama () hier gar nicht brauchst und die double-bignamaf) auch für den int-Funktionsaufruf verwenden kannst, weil dann eben eine Standard-Typ- umwandlung von int nach double durchgefuhrt wird. Also zwei Fliegen mit einer Klappe geschlagen! bigmamaO /***•> Ja «*»$/ Mutt drent Ich weiß Schrödinger, wir sind alle ein bisschen faul. So haben auch die Leute gedacht, die sich C++ ausgedacht haben. Darum musst du. wenn du nicht willst, nicht dreimal den gleichen Code schreiben. Aber bis ich dir die Templatefunktronen erkläre, das dauert noch ein wenig. 228 siC8(N
Am Ende der Geisterwelt Gratulation! Du hast das Kapitel zu den Funk- tionen gemeistert. Ein weiterer kleiner Schritt zu großen Taten in. deiner Schuhkanon-Fabrik, Die großen Schuhkonzerne werden sich noch um dich scharen, und du wirst es weh bringen. Hey, Dagobert! Aufwachen! Du, mit deinen TagrrJmmen. Denk dran, dass du den Müll noch rausbringen musst, bevor deine Freundin nach Hause kommt. Zum Abschluss will ich aber noch ein paar Tests mit dir durchführen. (EimUeh* Aulgib*] Betrachte dir die folgenden Funktionsprototypen mit Standardparametern Welche dieser Prototypen sind falsch? Nicht auf die Lösung gucken, gell?* void endlichfercigCint, int“0, int"l); void keinelustaehr(int«2, int«O, int“l); void waskorantdanach(lnt“l, int, int); void ichul1Iprogrammierenfint“2( int, int“l>; void dasletztebeiapiel(int=O, int»l, int); *1 Richtig , das ist einfach Pfft void void void void void endlichfertigfint, int“0, int“l); *1 keineLustnehr(Int “2, int-O, int“l); *2 ' waskommtdanach(int»l, int, int); *3"- lT ichwllLprogrammierenfint»2, int, int«=l);^^^ dasletztebeispieltint*0, int»l, int); *2 Richtig* Mal schauen, ob du auch die Funktionsüberladung verstanden hast: ISc hwitript In den folgenden Zeilen findest du fünf überladene Funktionen. Welche dieser über- ladenen Funktionen ist richtig und welche falsch? Augen weg von der Lösung? ) i ); int overloadedf inc ); int overloadcdf const int ); double overloadedt double» double void overloaded( ); void overloadcd( vcctor<int>» inc Funktionen 227
Gar nicht schlecht! Aber wenn du das in einem Code ausprobiert hättest. hätte sich der Compiler beschwert, dass die Funktion int ovetloaded( int) zweimal vorhanden ist. Der Fehler liegt hier am const- Paramctcr, der zweiten Funktionsübcrladurig. Der COUSt-Qualifizierer wird nämlich bei einem Parameter der Funktionsüberladung nicht ausgewenet. Zwar wurde dieser Qualifizierer noch nicht erklärt, aber das sollte hier doch demonstriert werden, weil solch ein Fehler für ziemliche Verwirrung sorgen kann! Aber du konntest das mit deinem bisherigen Wissen nicht kennen, ganz klar! Jetzt wird's Zeit, dass wir zum Schluss kommen. Apropos Schluss! Im folgenden Listing kann. Je nach Eingabe, das Programm an drei verschiedenen Stellen beendet werden. (Etarfach« Aufgabe] Erklär bitte, wann welches Ende aus- gefuhrt wird. Sollte eigentlich leicht für dich sein! void aufwiedersehen( int ); int main() { int dahaste; cout « "Gib ihm die Zahl: if( ! (ein » dahaste) ) IP > ’1 ente Ende return 1; *1 > aufwiedersehen(dahaste); cout « "Pfitigott\n"; return 0; *3 j *3 d« tetrte Ende void aufwledersehen( int n ) ( if(n < 255) ( cxit(n); *2 > } f Das erste Ende gibt es, glaube ich wenn bei er Anwender einen un- gültigen Wert, wie bspw. einen Buch- staben, eingegeben hat. Das zweite Ende gibt’s, wenn der eingegebene Wert von iner als 255 ist. Und das letzte Ende gibt es eben, wenn HM die anderen beiden Enden zuvor nicht in Aktion treten durften! danas ’2 das zweite Ende Prima! Jetzt hast du das Kapitel zu den Funktionen geschafft Vielleicht solltest du das mal zusammen mit deiner Freundin bei einem Glas Sekt feiern? Bring aber vorher den Müll raus ...! 228 co.t.t sicetw
> ||«l Schlüsselwörter I für Typen, Naniens- IC8MWim' bereiche und die Präprozessor- |Ke aus Chatos Ordnung entsteht In diesem Kapitel lernt Schrödinger verschiedene Pröprozessor- snwelsungen kennen. Dabei erführt er, dass der Pröprozessor ein eigenes Programm Ist, mit dem er zwar eigene BASIC-Befeh- le nachimplementieren könnte, welches allerdings noch weniger Ahnung von C++ hat als Schrödinger selbst. Hierbei lernt er auch, dass es bessere Alternativen zu der Makrosprache C++ gibt, es aber In der Praxis nicht ganz ohne den Präprozessor geht. Seitdem Schrödinger Immer mehr Fortschritte macht und die Programme umfangreicher werden, merkt er, dass es Immer mühsamer wird, den Überblick über den Code zu behalten. Da sich Jetzt auch der Compiler Immer öfter beschwert und der Lin- ker gelegentlich komplett den Dienst verweigert, wird es Zelt, Schrödinger zu beruhigen und Ihn wieder an die Hand zu nehmen. Bel einer schönen Tasse, nein besser einer schönen Kanne grünen Tee (Sorte Bunpowder), wollen wir Schrödinger zeigen, wie er seinen Code besser organisieren kann.
Heule müssen wir ein paar weniger spektakuläre, aber für die C++-Programmierung sehr wichtige Schlüsselwörter berufen, welche du vor andere Typen oder Funktionen schreiben kannst. -1^1 H ^Jg. OT. 2. /£^X. * £^\_ <4,1 00? z Wenn du gelernt hast, deinen Code bes- ser zu organisieren, geht es in die gewünschte Richtung los. Also: .Einmal bleiben wir noch wach, heißa dann ist ______ OOP-TachJ* Hurra ... *konfettischmeiß*! "* Also, reiß dich nochmal zusammen! Klasse, die Speicherklasse AMt den Schlüsselwörtern extern und Static kannst du entscheiden, wo deine Typen im Speicher abgelegt werden und/oder wie lange sie dort lagern sollen. Außerdem gibt es noch die Schlüsselwörter auto (hat seit C++11 eine neue Bedeutung) und mutable (ist jetzt noch zu kompli- ziert, um es zu erklären). Neu ist außerdem noch thread_local (welche in Ver- bindung mit Threads verwendet wird). Das Schlüsselwort extern ... ... benötigst du, wenn du eine globale Variable in der Datei Abba definierst und diese auch in der Datei Saab verwenden willst. Der Compiler erhält damit quasi den Hin- weis. dass er die globale Variable in der Datei Baab verwenden will, und erteilt dem Linker den Befehl, dass er diesen Verweis in einer anderen Einheit (der Datei Abba) auflösen muss, well diese ja woanders definiert wurde. Ohne das Schlüsselwort extern in der Datei Baab würde dir dieses Mal nicht mehr der Compiler einen Strich durch die Rechnung machen, sondern jetzt der Linker. Und solche Linker-Fehler sind wesentlich schwieriger zu finden. 230 *cxt
(Hintrrjrundinio) Eine Funktionsdeklaration wird übrigens vom Compiler automatisch als extern behandelt. Datier musst du bei einer I» • 1.. -v r Funktionsdeklaration nicht zwangsläufig das Schlüsselwort extern verwenden. Es ist aber auch nicht falsch! f iner jlotulfri Variablen. dir <ib<r mthr^nr Oil«««n verwerfet werdwi seil, braucMt du auf jeden Fall dis $chluM.rlwQrt CXÜCrfl. wnji hat der IMer eia Ptoblem, vueil et djinn f^eirnjl duselb* Symbol Auflöie« mv»$tc rMlwrteh ein l eMe* iit)! |AcMun<| Verzichte am besten gleich, wenn mög- lich, auf solche globalen Variablen und Objekte, und du ersparst dir hiermit den Murphy-Fall - die Suche nach einem schwer auffindbaren Fehler! Das globale Schlüsselwort static ... ... vor einer Variablen oder einer Funktion beschränkt die Gültigkeit auf eine Datei. Globale Stat ic-Variablcn können bspw. dazu verwendet werden. Namenskonflikte zwischen gleichnamigen Variablen in unterschiedlichen Dateien zu vermeiden. Eine mit static gekennzeichnete Variable wird außerdem automatisch mit 0 für den passenden Typen initialisiert. Eine solche dateilokale Gültigkeit lässt sich aber auch mit einem anonymen Namensraum inameSpace) einrichten. wort static ... Das Schlüsselwort hat also einen doppelten Boden und sorgt somit für Verwirrung. Verwendest du nämlich die static-Variable innerhalb eines Anweisungsblocks, bekommen diese einen festen Platz im Datensegment. Das bedeutet quasi, du kannst die Static-Variable innerhalb einer Funktion verwenden und gar -Ja, die Todsün- de - als lokales Speicherobjekt an den Aufrufer zurückgeben. Und es kommt noch besser Ein lokales Static innerhalb einer Funktion merkt sich den Wert auch nach dem Ende der Funktion. Schl Utftuort f für Typ»n. fr.) p m&«r « j c h » ur.q di» Pr>proi » >l<r • > i r »«t > v*r 231
(Achtun*| , Natürlich muss hier noch angemerkt werden, dass dieses Erhalten und Merken des Wertes nach wie vor auf den Anweisungsblock beschränkt ist, in dem das Spei- ' cherobjekt definiert wurde? ^ze/ ? Ja, leider! l>a$ statische globale Static kann verwendet werden, um die Gültigkeit von Variablen oder Funktionen inner- halb einer Quelldatei einzuschränken. Das statische lokale Static innerhalb eines Anweisungsblocks funktioniert quasi als Gehirn einer Funktion, welche nichts mehr vergisst und dauerhaft vorhanden ist. In einer früheren Zeit gab es noch das Schlüsselwort register, womit man Vor- schlägen konnte, dass eine Variable im schnellen Prozessorregister anstatt im langsameren Arbeitsspeicher abgelegt wird. Da es sich nur um einen Vorschlag handelte und viele Compiler ohnehin selber entscheiden, wie sie den Code opti- mieren. wurde das Schüsselwort mit C++11 als „deprecated” erklärt, was für dich bedeuten soll, dass du dieses Schlüsselwort nicht mehr verwenden sollst. Und was war das noch mit auto? Wenn du rumgegoogclt hast, wirst du 4ich vermutlich fragen, wo das auto Schlüsselwort geblieben ist. Im Grunde ar das Schlüsselwort ja ohnehin über- flüssig, weil auch ohne Voranstellen des Wortes auto eine Variable automa- tisch auto war. Im neuen <+♦-Standard kann das Schlüsselwort verwendet wer- den, damit automatisch ein Datentyp ausgewählt wird. Welcher Typ das dann ist, hängt von der Zuweisung ab. Mit den Schlüsselwörtern const und volatile lassen sich bei der Deklaration die Typen noch etwas abändern. Wobei volatile in einem normalen Eiaushall eher selten benötigt wird. Das Schlüsselwort const « Objekte, die du mit const verzierst, können nach der Defini tion im Speicher nicht mehr verändert werden. Damit verpasst 232 ACHT
du dem Objekt quasi einen Schreibschutz. Versuchst du einer mit const geschützten Variablen trotzdem einen neuen Wert zuzuweisen, beschwert sich der Compiler hei dir. const string wathatdu = "Osterei.'*; *1 wathatdu - ‘'Osterhase*1; *2 ’2 Der Compiler das nicht wollen und meckein1 •i wathatdu i$t nur lesbar und darf nicht beschrieben werden. Das Schlüsselwort volatile ... ... arbeitet eher auf einer etwas tieferen Ebene und dürfte von den meisten kaum benötigt werden, Ich zwing mich jetzt trotz- dem. ein paar Zeilen dazu zu schreiben. Eine volatile Variable kann von außerhalb des laufenden Programms einen neuen Wert erhalten. Damit könnte praktisch eine volatile Variable vom Betriebssystem, der Hardware (per Interrupt) oder auch anderen laufenden Programmen einen neuen Wert erhalten. Du siehst schon (oder auch nicht...). worauf das hin IZfittl] Nützlich und gerne verwendet wird const auch bei den Parametern von Funktionen, um ein versehentliches Ändern des Wertes innerhalb einer Funktion zu verhindern. Bspw.: void func(const int& val); Hiermit stellst du quasi sicher, dass val in der Funktion nicht mehr geän- dert werden kann. ausläuft. volatile ist als Hinweis an den Compiler zu verstehen, dass er auf Lese- und Schreibvorgängen der volat lle-Variablc keine Optimierungen durchführen darf (was er ja gerne macht). Die Compiler sind in dieser Hinsicht nämlich sehr intelligent und vorlaut. Da wird schon mal aus Optimierungsgründen eine Zuweisungaus einem alten Zwischenspeicher (Cache) des Prozessors verwendet. Bei einer Treiberprogram- mierung Ist ein derartiges Verhalten aber unerwünscht. Hier brauchen wir keine alten Zustände der Hardware, sondern stets die aktuelle Zuweisung. Mit volatile erzwingst du praktisch, dass die Variable frisch aus dem Hauptspeicher eingelesen wird und nicht aus Irgendeinem vorlauten und optimierten Speicher! Auch Ihr die Funktionen gibt es noch spezielle Schlüsselwörter, um ihre Eigenschaften zu ändern. Mil inline hast du ja schon eines kennengelernt. Dann gäbe es da noch die Schlüsselwörter virtual und expllcit. welche im Zusammenhang mit Klassen und deren Elementfunktionen stehen. Daher hebe ich mir die passende Beschreibung für den gegebenen Zeitpunkt auf. |Ze4t<n Ebenfalls neu in C++11 wurden die Attribute override und final ein- geführt. welche im Zusammenhang mit dem Schlüsselwort virtual verwendet werden. Deren komplexe Bedeutung hier jetzt zu erklären, macht wenig Sinn. Hier sind gute Kenntnisse der Klassen (-Hierarchien) erforderlich. tchlümlnort»'- für Typen, h» » ntt>»r» j c h » wnq di» -tirKtlvtr 233
Mindesthaltbarkeitsdatum ändern Ja, Ich weiß, das waren jetzt viele neue Schlüsselwörter und das war auch viel theoretisches Herumgeschreibe. Daher bekommst du jetzt die Beispiele nur so um die Ohren gehauen. Das Schlüsselwort extern in der Praxis ... {I Datei: Abba.epp int globalesDing; void putoutO; *3 int main() 'S Die Funktion putout ( ) wi<d rufen die Quelldatei Abba . epp mit dem H.iuptpro^rjifnm dir globale Variable globalesDing SH globalesDing “ 123; cout « globalesDing « endl; putoutO ♦ return 0; •3 Eine Funktions- deklaration O r Deftniteo«! des Codes befindet skh in eine* anderen Quelldatci *4 Die ßtob.ilr V.irtA wird m»l einem Wert (123) initialisiert und abgegeben (Code taubeiten) Aufgrund der besseren Kennzeichnung und Lesbarkeit würde ich dir hier empfehlen, extern auch vor die Funktionsdeklaration putout() zu setzen, um dem Leser des Codes mitzuteilen, dass die Definition irgendwo anders erfolgt. Intern macht das ja der Compiler für dich. 234 O.it.l ACH?
*3 bei der wir natürlich nochmals die globale VaraWe verwenden wollen. II Baab.cpp cout « globalesDing « endlj "2 Hier ist s e nur». d>e Definition der Funktion putoutf) Am Code kannst du wohl schon erkennen, dass da noch eine Quelldatei mit der Funktionsdefinition fehlt ... extern int globalesDing-, void putout()M^^^^ M HM!r finde« du d ic globale Variable globalesDing von Abba 4 epp rrneiil Nur jetzt ab extern deklariert, Oer Compiler wt»ß jetzt . Ah-Är das hier Ist nur die Deklaration, und die Definition befindet sich woanders!4 Natürlich mir» der Compilei den Linker darüber rnfonnierem .Hey. Link da* globale Ding habe ich woanders hingelrgl1 Nimm da* bitte okayr IFehWMüll) Ohne das Schlüsselwort extern würde der Linker am Ende vor zwei Variablen mit dem Namen globalesDing stehen und könnte seine Arbeit, eine ausführbare Datei daraus zu machen, nicht zu Ende führen. Schimtluortt'- für Typ + n-, N j p n « j c h P qjg Prjpr orgnor ) | r q< t 235
Beim Übersetzen von Abba.epp und Baab.epp musst du natürlich beide Quelldateien mit angeben, um diese zu einer ausführbaren Datei zu machen. Weißt du nicht, was ich meine? Ich erkläre es dir ein paar Seiten später! Jetzt wurde CXtCm durch ... und wenn wir jetzt static statt extern verwenden? Static ersetzt Die Variable globalesDing n Baab.epp ist jeU1 nur noch in der Date Baab . epp gültig- Abba .epp verwendet jetrt II Baab.epp <. static int globalesDing; *1 void putoutO { cout « globalesDing « endl; > rin r njenes glokilm globalesDing Mit dem Linker gibt e5 Jetzt auch keine Probleme ’2 Hier wird natürlich dav dateilokale globalesDing aus der Datei Baab. epp verwendet und ausgegeben. O.i static Va-iuMen automatisch mit 0 initialisiert werden, wird hlcf auch 0 au^eßeben ... und jetzt nochmals static als Gehirn H i-r steht die Funktionsdefinition der Funktion zuf alIsgenerator () long zufallsgenerator(int x=O)Bl static long anfang 123 anf ang+»x; anfang-(anfang*anfang) X re turn anfang; *5"^ 3333-1 Hier bndest du die lokale statische Variable anfang, welche zuallererst mit dem Wert 123 inilulirnTt wird. 3 Wenn du der Funktion einen Parameter fir x übergibst. wird d <“.^r Air statischen Variable aufaddiert Oer Standardparameter <$t hierbei 0. falls nix übergeben wurde Zurückgegeben wird der neu generierte Wed von an fang « endl;Mi cout « zufallsgeneratorf) cout « zufallsgeneracor(33) « endl;l cout « Zufallsgenerator(12} « endl;l M Jetzt fuhren wir eine dubiose Rechnung (ohne wissen- schaftlichen Hintergrund] mit der tf.i!>uhen Variable anfang durch und weisen das Ergebnis gleich wieder an fang zu J '6 Hier wehen drei verschiedene Funktionsaufrufe, welche alle unterschiedliche Zufallszahlen wigeben werden 238 C«p‘.f 1 ACHT
1 Noriri Dass immer unterschiedliche Zahlen ausgegeben werden, ist der statischen Variablen an fang zu verdanken, welche ja bei jedem Funktionsaufruf den vor- herigen Wert nicht mehr kennt und praktisch immer wieder mit der zuvor erstellten Zufallszahl neu arbeitet. |Cod* bearbeiten) Mit C++11 bietet C++ jetzt auch eine eigene Möglichkeit an, Zufallszahlen zu erzeugen. Der Zufallsgenerator besteht hierbei aus zwei Teilen: dem Generator selbst, der eine Reihe von Zufallszahlen erzeugt, und einer Verteilung, welche diese Werte in einem vor- gegebenen Bereich verteilt. Damit nicht immer die gleichen Zahlen verwendet werden, wird eine sogenannte seed (ich übersetze das jetzt besser nicht) benötigt. Hierzu ein Codeausschnitt, wie Sie echte Zufallszahlen in C++11 erzeugen können (hier im Bereich 1-10000): linclude <randoca> !( Zufallszahlen mit C++11: cout « "C++II:" « endl; random_device seed; mt19937 engine(seed()); // Generator uniform_int_distrlbution<long> valuesfl, 10000); II Verteilung 1-10000 for( siÄCt i-0; 1 < 3; ++i) { cout « values(engine) « endl; > rur T/ptn. ynp di» Pr>prp t >>>«< • > i r »« t > v»r 237
Glelch-GUItigkeitsberelch So, das waren jetzt viele neue Schlüsselwörter, die du hier verwendet hast. Ich bin gespannt, ob du dir deren Sinn und Zweck merken konntest. |finfachf Auifib«] Nenn mir kurz die Schlüsselwörter, mit deren Hilfe du Einfluss auf die Lebensdauer und Bindung von Speicherobjekten neh- men kannst! Hier die Schlüsselwörter für die Speicherklasseneigenschaften: w extern - Das Schlüsselwort solltest du für globale Speicherobjekte einsetzen, welche über mehrere Quelldateien verwendet werden sollen. Damit wird dem Linker quasi mi[geteilt, dass er dieses Speicherobjekt woanders auflösen soll. Andernfalls würde es bei zwei gleichen Bezeichnern zu einem Konflikt kommen. * Static (1) - Das Schlüsselwort kannst du ebenfalls verwenden, um eine globale Variable auf die Gültigkeit der Datei zu beschränken innerhalb derer sie defi- niert wird. Damit kannst du praktisch Namenskonflikte vermeiden, wenn es meli rere gleichnamige Bezeichner In unterschiedlichen Dateien gibt. » Static (2) - Innerhalb eines Anweisungsblocks (bspw. Funktionen) erhält das Speicherobjekt einen festen Platz im Datensegment und merkt sich sogar den Wert über die gesamte Prograinmausführung. Die Gültigkeit eines solchen Spei cherobjektes beschränkt sich natürlich auf den Block, in dem es definiert wurde. Wegen der Doppeldeutigkeit von static würde ich auf die Verwendung des globalen static zum Beschranken des Gültigkeitsbereiches innerhalb einer Quelldatei verzichten und stattdessen auf einen anonymen Namensraum zuruck- greifen. 238 Up'.ttl ACHT
Hm, ich dachte, dass hauen wir schon kurz beschrieben? Okay! Das neue alte Schlüsselwort auto kann im neuen C*+-Stan- dard wie folgt verwendet werden: auto einWert = 1234; auto nocheinWert • 3.1415; Wie du siehst, wurde bei der Zuweisung auf den Typ verzich- tet. Dank des neuen alten Schlüsselwortes auto hängt der (automatische) Typ von einWert und nocheinWert von dem Wert der Zuweisung auf der rechten Seile ab. Intern würde der Compiler diese Zeilen wie folgt umwandeln: int einWert = 1234; double nocheinWert • 3.1415; (Achtuflf/Marsichil Damit dies allerdings auch wirklich funktioniert, musst du dieser auto-Vanablen bei der Erzeugung gleich einen Wert zuweisen. iNotkfpni'Ubenl Damit die neuen C++-Standard-$achen überhaupt funktionie- ren, brauchst du auch einen Compiler der neuesten Art. Beim GCC ist der neue C**-Standard bereits größtenteils ab Version 4.6 integriert. Bei vielen Compilern kann es allerdings sein, dass du das „C++11-Verhalten“ erst einschalten musst. Im folgenden Code wird ein ganzer Zeichensalat von Fehlermeldungen ausgegeben. Was wurde hier falsch gemacht? void readmefconsc string! s) < cout « s « endl; s "hasch mich!"; > • 44 string str(”Huhu”); rcadnic(str); cout « str « endlj Sch] fi»r Typpr,-. hf.j p n » j c h p cH p 4i t 1 239
Die Fehlermeldung wird beim Übersetzen ausgegeben Nein, et isi keift MAtfiaHiflUfgrundbilt# ... ♦r>. •T<jsF^ki|.c a':i;nq< CT-l4rTr _Ts*ltaJ r lr^< erwar Mtch> /ept LocalZlacl .*4a/^cc4 Vc« Xbm.'teanac nt r 4 nQ. h: $11 s-?! note: atd: :bulc_nt r Ln?<_C3L«TT, _Trnit«, _A-lloc>fc »td: :toanic_ntr Ir^KChazT« _TiAlt«r Loc> : :cp®rator-(_ChAET) |ultb ZHaiT char« ^Trakte • »td::ehnr_*r*Lt»<rh*r>r _A1L6c • itd::nllQcatoT<char>* 4t d:: bolr_3trär*»^<_Ch*fTrl _Trait>4 _Jkllce> • ntd: tba3ie_»t£lh$<eh«f >) <fttar MltCh» /«C^^iocak/iACl<4e/*cc4&/c«»/bkiA/tet»»« »ti1hiS44i?i nocet <r«i ib«4ic_«rrkr>f<w0MrT. _Tnlt«. wA])oc>k *r<ii ilM«-icw»xr ir-j-cjThAi?« Tr-tb?», „AlLoc’i iep«M'9c-i«-rdi ttoa»ic,r*t ri«*9<.aCMrT* rtlti, _JUl>r>4.ll |wlth _OurT - <Karr _Tr*u.i - a«:4:: r*5«*«h ar>. _A11qc • ntdt :aL l&.-atorcc.'xar >, atd: tipaasewt rln^t rKat?« TiAit r _A11qc> • «td: ;baair_«t rtnyCrhari] <naar Mtcts) Zopt / locak/lrcl oda/ipcc45 /c« /bä«/teai>c_nt rtray. h:$?•:?: not«: • td: :baaie_at r Lr^^CSaaT, _Tralta« _Aäloc*b ntd: :toanic_ntx CbarT« Taakta« Alloc': :®para*or-(nid:: initialkxar llat^ CharT >J («Ith _Ch*rT • thar« _TtllU - Atd: tthar t rnitncc^af >. _A11« - a tat iaHfreai4r*ihai>, »t-li tlr»<<_rM«7* _?raita^ Jüiöe» - M Ci iba4ies4trbM<<h«i>| Dbaiar Mar I Q Enter the Lösung... **«// COHSt /h t>6-f A-^x47'{^i f . Ia/^F CjA A« KO^rZrC/C && COnSt IZe tief] Zum Haarespalten: Em Objekt, das du mit const kennzeich- nest. ist nicht wirklich eine Konstante! Mit const wird das Objekt nur als unveränderbar im Speicher markiert und darf somit nicht mehr auf der linken Seite einer Zuweisung stehen! An der Stelle habe ich noch etwas vergessen, und zwar die Ver wendung vom Schlüsselwort extern bei const-Variablen. Weil const Variablen nur in der Datei sichtbar sind, in der sic definiert sind, musst du hier extern verwenden, damit diese Variable auch in anderen Dateien gesehen wird: ff Abba.epp extern const int globalesDing 123; '1 ff Baab.cpp *1 Deklaration und I mlahs*crung ( Definition! so^ic das Schlüsselwort extern sind in der Quelldatei Abba.epp unbedingt nötig* Probier’* nu.1 auvt lass einfach -rum testen extern weg und genieft die fehlermeWung "2 Die Deklaration ohne Delinition, damit der Linke* extern const int globalesDing; ‘2 void putout() { cout « globalesDing « endl; *3 > e«n^n Verweil Auf dM bereits brrwnnu globalesDing erhalt. *3 Jetzt k.ifin die gtob.ile Konstante aus Abba * Cpp auch in Baab ♦ epp verwendet VL werden. IBrloh'wng/'ltaucig] Prima, einen trockenen Abschnitt hast du jetzt schon hinter dir. Vermutlich ist er dir wie die Wüste Sahara vorgekommen. Genehmige dir doch einfach eine Kanne grünen Tee, damit du für den nächsten Abschnitt in der Wüste Gobi gerüstet bist! 240 Ofrltal ACHT
Tausendmal gesehen und tausendmal nicht beachtet, wie der Flie- genkadaver an der Wand, den du im letzten Monat erlegt hast. In diesem Abschnitt wird es endlich Zeit, die Anweisung using und das Anlegen eines neuen Namensraumes mit name Space näher zu betrachten. Das Thema ist recht einfach, denn im Grunde geht es hier nur darum, einen neuen Lebensraum für Speicherobjekte wie Variablen, Klassen, Funktionen usw. zu schaffen, um einen Namens- konflikt mit vielleicht vorhandenen gleichnamigen Spclcherobjekten zu vermeiden bzw. zu beheben. Dann leben alle glücklich und zufrieden bis an ihr Frogrammende. Nehmen wir mal an, du hast in deinem Programm folgende Bezeichner: fl MeinProgratni.cpp unsigned int cisbaer; unsigned int elefancen; unsigned int gorilla; dass Sacugcticr { }j Jetzt findest du Im Web eine Bibliothek, mit der du dein Programm um weitere nützliche Funktionen erweitern kannst. Hier ein Ausschnitt aus der Headerdatei der Bibliothek: // SavetheAnlt&als-h unsigncd int clcfantcn; unsigned int gorilla; unsigned int loewe; dass Saeugcticr < }; Was jetzt kommt, kannst du dir sicherlich schon denken? Genau! Im Grunde würde man(n oder Frau) Folgendes machen, um die neue Bibliothek {in dem Fall nur eine Headerdatei) In deinem Code zu verwenden: SchlÜi»*Lwört«r für hM »nifc«r»ich» und di» o Z 01 icr > *1 r 9 k t & 241
II MeinProgratcm.cpp linclude "SavetheAnima1s<h" unsigned int cisbacr; unsigned int elefancen; unsigned int gorilla; dass Saeugetier { >; Dass es Jetzt zu einem Konflikt mit den gleichen Namen kommt, sollte dir klar sein. Ako brauchen wir eine sinnvolle Lösung! Bei kleinen Projekten kannst du das vielleicht machen, aber bei ein paar tausend Zeilen Code über mehrere Quelldateien wird das recht mühevoll. Und wem» dir nur einer entwischt, gibt es wiedereinen Matrixbildschirm heim Obersetzen. Nein, C++ hat ein besseres Mittel dafür: Ei inen solchen neuen Lebensraum kannst du mit dem Schlüsselwort namespace wie folgt schaffen: namcspace Namc_fucr_neuen_Lebcnsrauni < II Definitionen und Deklarationen des neuen Lebensraumes } Bezogen auf unser Beispiel hast du jetzt folgende zwei Möglichkeiten: Du setzt den Header in einen neuen Namensbereich: II MeinProgratEtn.cpp namespace neuerLebensraum < linclude '’SavetheAnimals.h" } unsigned int eisbaer; unsigned int elefanten; unsigned int gorilla; dass Saeugetier ( >; 242 ACHT
Oder du spendierst deiner Quelldatei einen neuen Lebensraum: // MelnPrograuxa.cpp linclude "SafetheAnimals.h” namespace Sch roed inger »Lebens rautn < unsigncd int cisbaer; unsigned int elefanten; unsigned int gorilla; struct nahrungsmittel { string Bezeichnung; unsigned int gewicht; >; dass Saeugetier ( }; -- 13ti |HiAttr£rurtdin*0| Die Anzahl der Name ns bereit he, die du in deinem Code verwenden kannst, ist nicht beschränkt Du kannst in.eine Quellcode durchaus mehrere unterschiedliche Namensbereich definieren. Kein Problem! In dein Fall werden die Deklaration und die Definitionen im bereits vorhandenen Namensraum um die Deklaration und die Definitionen des erneut verwendeten Namensraumes erweitert. Gibt es hier doppelte Namen, kommt cs natürlich wieder zu Ausschreitungen bei der Übersetzung. Der Zugriff auf einen solchen Lcbensraum muss natürlich auch wieder über dessen Namen gehen. Dafür brauchst du nur den Namensraum, den Zugriffsopcrator :: und dann noch das gewünschte Speichere bjekt. auf das du zugreifen willst. ryr Typ»»-. ^»O dl» Pf-ipro t m-?r • > j r »« ; j v»r 243
Ein Beispiel für einen solchen Zugriff: ff MeinProgram.epp linclude "SavetheAnimals.h" *4 namespace SchroedingersLebensraum "2 { unsigned int eisbaer; unsigned int elefanten; unsigned int gorilla; struct nahrungsmittel { string bezeichnung? unsigned int gewicht; dass Saeugecier { }; > • « • SchroedingersLebensraum::eisbaer 20000; *3 SchroedingersLebensraum:;elefanten 500000; *3 gorilla - 90000; loewe 30000; M SchrocdingcrsLebcnsraum;;$acugeticr dclphin; *5 *1 Die DcfcUrjktion^n und Definitionen der fremden Bibliothek belassen wir m Ihrem natürlichen übewaum. "2 Unsere eigene Datei schränken wir auf de-n Lebensraum SchrocdingcrsLebcnsraum ein. •3 Zugriff auf cisbacrund elefanten im Namensraum SchrocdingcrsLebcnsraum *4 Zugriff auf di«e V.iruH«* unserer fre-mden Bibliothek ,ika der Hfarferdatel .SavetheAnimals.h "5 Qeliuilion eines Objekte* «n Typ Saeugetier jius dem Namensraum SchroedingersLebensraum Natürlich ist der Weg über den Namensraum und den ZugrifFs- operator nicht so gemütlich. Wie du cs aber von Std: : bereits kennst, gibt es auch noch einen anderen Weg, einzelne oder alle Mitbewohner aus einem Lebensraum vorzustellen. Hier hast du zwei Möglichkeiten. Eine davon wäre: using Lebensraun::Mitglied_des_Lebensraumes; Damit kannst du praktisch ein einzelnes Mitglied aus dem Lebensraum sichibar machen. Zum Beispiel: 244 c*o; ttl ACHT
using SchroedingersLebensraum::elefanten; ’1 M Für elefanten und gorilla wird itend.ndm.iSig der Njmensr.ium SchroedingersLebensraum gcvrtrt using SchroedingersLebensraum:zgorilla; ’1 *2 Der Zugriff auf eisbaer erfolgt nach wie vor über den SchroedingersLebensraum::eisbaer elefanten 500000; *3 gorilla - 90000; *3 ::gorilla - 95000; "4 loewe • 30000; *5 20000* ‘2 Namewaum uMden Zugftffsoperatoi. •3 Da eleganten und gorilla zuvor mit using in den Narnrniraum geidzt wurden, erfolgt der Zugrifl .iuf elefanten und gorilla »n SchroedingersLebensraum jetzt ohne den Zugriftopcratür. ’4 Für den Zugriff auf gorilla un Header .SüvcthcAnimals.h* txaucMt du dann den Zugriffe’ Opeialor : :, weil dies ja jetzt dasglobale gorilla iS!. “5 Beim Zugriff auf loc WC wiederum brauchst du keine Zugriffeoperatofen oder den N.imentbereich, weil hie« ja dei Name im GüHigkeitsbcfeKh eindeutig ist lAcMun^l Ein Namensbereich ist ein echter Gültigkeitsbereich. Die Gültigkeit von using beginnt und endet innerhalb eines Anweisungsblocks ab { bis }. Deshalb ist es wichtig, dass du eine using-Anweisung auch an der richtigen Stelle platzierst, um einen eindeutigen Namen innerhalb eines Gültigkeits- bereiches zu erhalten! Willst du hingegen gleich alle Mitglieder deines Namensraumes verwenden, ohne Umwege zu gehen, kannst du eme etwas andere Art der using-Anweisung verwenden. Die andere Möglichkeit: using namespace Lebensraum: Daniil sichen dir ohne Umwege alle Mitglieder des Namensraumes Lebensraum zur Verfügung. Diese Art der Verwendung kennst du ja schon zur Genüge von using namespace std;, wie du cs in allen Program- men verwendet hast. Um also ohne Umwege alle Mitglieder aus SchroedingersLebensraum cinzubtcnden, braucht es nur folgende Codezeile: using namespace SchroedingersLebensraum; Schl f^r Tyc.»A, htj » n(»tr > | c h * Ji w Mcumor 245
Sich einen eigenen Lebensraum zu schalYen, ist eine tolle Sache und ein ordentlicher Weg, auf die Globali- sierung zu verzichten. Rci globalen Dingen gehl es im Grunde immer um die Durchsetzung einzelner und großer Interessen. Die Masse bleibt dabei auf der Stre- cke. Bei einem eigenen Lebensraum kann jeder mit denselben Interessen bestehen bleiben, und bei richti- ger Anwendung kommt es zu keinen Konflikt. Doch sind wir, die Namensräume lassen sich halt recht revolutionär beschreiben! Nun ist es wieder Zeit für die Praxis ... Hier definieren wir /Include <lostream> . Lieblingstiere namespace namespace Lieblingstiere -( jJr *3 Logiuherweiie mu«i du bei der Funktion*- definition drn N.irn<‘mr^um und den ZugritfwpcMtOf verwenden Sonst würdest du Mer tediglkh eine neue globale Funktion Output defin'»?re«B^^i D.i du bei dec Definition den N.imeniraum bereit! .ingegeben halt, brauchst du jetrt bei den Mitgliedern dei Namrnsraumri LieblingStieire krinrn Nürncnv.ium und Zugfiffcoperator mehr zu verwenden, unsigned int locwc; unsigned int elefanc unsigned int eisbaer void Output(void); ’2 Hier liehU du eine Funktionsdcklaration im Namensraum Licblingscicrc void Lieblingsciere-:output(void) *3 < - cout « "Löwen cout « "Elefanten cout « "Eisbären • ' « locwe « endl; *4 • ' « elefant « endl; *4 ' « eisbaer « endl; *4 > Der komplette Namensraum Licblingsticrc ist jetzt in der main-Funktion eingeblendet und kann dort ohne Umwege verwendet werden. int main() < using namespace Lieblingstiere; Zf *5 248 Üpittl ACHT
•6 Hier edolgt der Zugriff auf die einzelnen Mitglieder von Lieblingstiere Hier die Headerdatei: II BedrohteArten.h linclude «iostreao» namespace BedrohteArten { unsigned unsigned unsigned unsigned unsigned unsigned unsigned void Output(void); int int int int int int int elefant; gorilla; loewe; Schneeleopard; orang_uthan; buckeIwal; eisbaer; loewe 30000; 6 elefant 500000; '6 eisbaer 22000; outputO; *7 return 0; *7 Der Funktions- aufruf von Output ( ) braucht .iuch keine ipejiellefi Zugfiffvoptionrn, um ausgefuhrt zu werden (Einfache Aulgab*] Meine Absicht war es natürlich nicht, dir hier einfach so ein Listing hinzuklatschen* Nein, du sollst dein Programm nun um den Header BedrohteArten.h erweitern. Dort ist ein Namensraum BedrohteArten enthalten, den du jetzt nicht auch noch komplett einblenden kannst, weil es sonst viele glei- che Namen gibt. An dieser Stelle darfst du selbst entscheiden, wie du vorgehen willst. Der Header BedrohteArten.h bleibt unverändert! void BedrohteArten::outpi«t(void) < using namespace std; cout « '•Löwen 4 ir « loewe « endl; cout « "Eieianten ii « elefant « endl; cout « "Gorilla ▼ tt « gorilla « endl; cout « "Eisbären 4 ir « eisbaer « endl; cout « "Schneeleopa rd *t ir « Schneeleopard « endl; cout « "Orang-Uthan 4 ir « orang_uthan « endl; cout « "Eisbären 4 w « eisbaer « endl; > Schl Un-t-tvor; t<- für Typ»n. f>.i » n « i c h » di» Pr>prot»i>ür•>lr«ltI»»r 247
Und jetzt eine mögliche Musterlösung linclude «loseres«* linclude "BedrahteArcen.h" using namespace std; •1 Dk Hcader- datei muss zuerst mit rein. namespace Lieblingstiere { void Output() ; using namespace BedrohteArten; } void Lieblingstiere::outputf) < cout cout COut > "Löwen "Elefanten "Eisbären using namespace Licblingstiere; loewe 30000; elefant = 500000; eisbaer 22000; Lieblingstiere::Output(); return 0; > II « loewe « endl; elefant « Clsbaer « “2 Hier winde alles gekürzt: Im Namespace Lieblingstiere wurde einfach der komplette Inhalt vom Namespace BedrohteArten «ingeblendct Daher brauchen endl; endl; Leidet kollidieren jetzt die beiden Funktionen Output ( ). weshalb wir h er nun die gewünschte Funktion des entsprechenden N imenSfiiumes mit Zugriffsoperator .Aufrufen müssen. Theoretisch konntest du hier .iuch die Funktion BedrohteArten: :Output() udrijcn Aber da würden viele nicht direkt Initialisierte Werte ausgegeben. I Notiz) Am Beispiel konntest du sehen, dass du einen Namensbereich (oder gegebenenfalls auch nur einzelne Mitglieder davon) innerhalb eines anderen Namensbereiches sichtbar machen kannst. Theoretisch kannst du natürlich auch einen Namens- bereich verschachtelt innerhalb eines anderen Namens- bereiches anlegen Allerdings wird dann der Zugriff wesentlich komplexer (bspw. Aussen: : Innen: :Mitglied). iFfhlcr/MUHl Das Schachteln von Namensräumen funktioniert allerdings nur innerhalb von Namensräumen. Willst du Namensräume in anderen Blöcken, wie bspw. einer Funktion, verschachteln. so wird dir der Compiler freundlich mitteilen, dass dies nicht erlaubt ist! int main() 248 ACHT
Anti-Globalisierung An sich Ist die Verwendung von using namespace xyz ; Ja ganz praktisch. Aber wenn man den Sinn von Namensräumen bedenkt, wobei cs doch darum geht, Namenskonflikte zu vermeiden, ist ein using namespace xyz ; eigentlich nicht immer optimal. Wozu etwas erst in einen Namensraum stecken, um dann wie- der alles öffentlich zu machen und gegebenenfalls wieder mit einem gleichnamigen Bezeichner aus einem anderen Namensraum zu kollidieren?! Klar, bei den ersten Schritten in C++ und den Beispielen im Buch (und auch in unzäh- ligen anderen Büchern und Beispielen im Web) findest du selten Std: : COUt oder Std: : endl, Aber In der Praxis? Kennst du den kompletten Inhalt von Std oder anderen Namensräumen? Oft verwenden wir gerne mal Bezeichner wie COUHt, sort oder find, und schon kann es zu einer Kollision kommen, weil es in der Standardbibliothek ein ähnliches Speicherobjekt mit diesem Namen gibt. Aber nicht nur in der Standardbibliothek findest du die. Es gibt abertausend andere Bibliotheken, und viele Programmierer verwenden nun mal Immer wiederkehrende. Übliche und logische (aber eben langweilige und wenig originelle) Namen, so dass cs immer wieder zu einer Namenskolliston kommen kann. Sc// jc/z/ std:: Jein! Zwar werden wir hier weiterhin den gesamten Namensraum Std einblenden, aber in der Praxis solltest du im Himerkopf behalten, dass es eigentlich nicht immer nötig ist. den kompletten Namensraum mit using namespace xyz einzublen- den. Oft brauchst du z.B. aus dem Namensraum Std nur COUt, ein oder endl. dann würde es auch reichen, wenn du nur diese Mitglieder mir using Std: : cout; usw. cinblenden würdest. iNotiz] Ein gängiger Weg ist es auch, einen Namensbereich nur in den entsprechen- den Anweisungsblock zu schreiben, Wie bereits erwähnt, ist eine using-Anwei- sung auch nur in einem Anweisungsblock zwischen { und } gültig. JEioüche Aufgabe] Okay, mal sehen, ob du verstanden hast, worauf ich hinauswollte, und nicht nur ein- fach mit dem Kopf genickt hast. Guck dir folgenden Code an, bei dem es zu einer Namenskollision aufgrund von std kommt und bei dem der Compiler wieder unnötig lange Fehler ausgeben muss, Kannst du den Fehler erkennen und beheben? SchlliJitNört ir für Typen, jb»r»ich| und di» o r9 S J«r >i r * k t & 249
Hier der Code: finclude <iostreatn> using namespace std; struct string { unsigned short groesse; unsigned short atueck. >; int main() < Okay, ich helfe dir! Die Klasse string, die du kennst, ist im Namensraum Std enthalten. Aiit der Struktur String, welche du hier erstellst, kommt es zum Konflikt mit der dir bekannten Klasse String. Um die Doppeldeutigkeit zu beseitigen, solltest du hier den Namenraum Std entfernen (also die Zeile using namespace Std ; kommt weg). Dann klappt es auch mit dem String Tanga. Wenn du Jetzt allerdings die Klasse String (ja. die für Zeichenketten) in deinem Programm verwenden willst, musst du aber den Namensraum mit Zugriffsoperator verwenden: Std: t String. Anonymer Lebensraum Bei Static habe ich es dir ja bereits gesagt, dass du mit einem anonymen Namensraum die Möglichkeit hast, auf Static für datellokale Variable zu verzichten. Schon vergessen?! Also statt ...
die bessere Alternative int malnO < anonym 1234; namespace { int anonym; > 1 ist e*n anonymer Namcjnsbcrcich und <kr ist in verschiedenen Quelfdalelen nicht von außen erreichbar. Nur innerhalb der Queltdatei, in der du diesen anonymen Namensberekh angelegt haut, ist der Intutt bekannt (•rlohnunfl Du hast prima mitgemacht in diesem Abschnitt. Aber denk dran, dass sich die Welt nicht nur um £♦♦ dreht. Also guck nicht immer nur geradeaus, sondern achte auch darauf, was neben dir passiert. Nachdem hier dauernd von bedrohten Tier- arten geschrieben wurde, kannst du dich ja mal auf der Web- seite http://wwf-aften.wwf.fc/ genauer darüber informieren. Ob du jetzt etwas spendest oder einfach nur ein Befürworter des Tierschutzes wirst - du hast das Gefühl, du tust was Gutes. Das setzt bei dir Endorphine, Oxytocin sowie die Neurot raus m Itter Dopamin und Srrotonin frei und beschert dir rin Glücksgefühl Und dieses Gefühl ist deine Belohnung! So, und jetzt füttere bit- te mal deine Katze! und dio >>i rpät 261
Lebensraum Im Wohnzimmer Die Namensräume waren ein recht umfangreicher Abschnitt, nicht? Von neuen Lebensräumen, Globalisierungs- gegnern über bedrohte Tierarten bis hin £u anonymen Welten war alles dabei. Und da sag noch mal einer. IT-Fachbücher wären langweiliger Schnarchkram! So, nachdem du frisch mit Glücks- Hormonen aui'geiankt bist, können wir Ja wieder eine Portion Stresshormone vertragen und das Thema mit ein paar Aufgaben abschließen. (Eimfache Aufgabe] Genau genommen kennst du jetzt drei Möglichkeiten, um auf Mitglieder eines anderen Namensraumes zuzugreifen Welche sind das? 1 Zunächst wäre da der Zugriff mit dem Namensraum ♦ Zugriflsoperator (bspw. Namensraum: xMitglied). 262 ACHT
2 Dann wäre da noch das Einblenden einzelner Mitglieder in einem bestimmten Bereich (bspw. using Namensraum: :Mitglied). 3 • Und zu guter Letzt wäre da noch dir Möglichkeit, den kompletten Namensraum einzublendcn (bspw. using namespace Namensraum), ’1 Hie* siegst du einen wahnsinnig hirnlos langen M-amcnsraurn? Einen Tipp habe Ich noch für dich, falls du den Namen eines Namensraumes mit einem anderen Namen (inn-)benennen oder verwenden willst: namespace unglaubllch_langer_namensraum { *1 If Deklarationen und Definitionen int binwaswert; * 3 Den Alias können wir fetzt jh Synonym für den OnginalrMmen verwenden. > namespace uln = unglaublich langer namensraum; *2 using namespace uln; *3 uln:sblnwaswert 1234; *4 * 4 Bel solchen Zugriffen und so hirnlosen AUmcn ist das wirklich von Vorteil * 2 Wir schaffen gleich Abhilfe und legen f&r ung laubl 1 ch_langc r__namen s raum einen Alias uln an. Damit kannst du praktischcrwcisc umständliche Namen von Namensräumen mit einem anderen Namen (genannt Alias) verwenden, oder du kannst verstecken, welcher Namensraum sich wirklich dahinter verbirgt. Dr« Möglichkeiten. etwj* 4 ui »iriwm Niwrnrium ru ww*nd»n khUnwluorU1* f**r Typ»A, hf.j » n »j c h » jnd qj» t’rjpr p ‘ > 1 rpl t 253
JSchwitrig« Bringe das folgende Beispiel zur Ausführung! Blende auf keinen Fall den kompletten Namensbereich std ein. Obwohl, das würde auch wieder zu einem Konflikt führen. Viel Spaß beim Tüfteln* linclude «lostreatn» linclude <string> void cout(const scringi print) { cout « print « endl; > Hier eine mögliche Lösung dazu linclude <iostream> linclude <string> Int mainO string putmcoutfShark me cout(putmeout); return 0; upl"); > void coutCconst std:tstringfc < _ using Std::COUt| *2 using std::endl; *2 cout « print « endl; *3 } print) '1 '3 Jetzt klappt e$ auch mit cout und endl *1 Hier hau du die Funktion COUt ( ) Wegen dieser Funktion kannst du kein globales using namespace Std ; verwenden. well cs sonst zum Niimcmkonflikt kommt, weil Std bereits ein cout entha: l Um hier den String mr Parameter zu verwenden, wurde außerdem der Namensbeieith mil Z u griffs- operator verwendet (std: :string: int main() < } std::string putmeout(”Shark mc upl”)? *4 CQut(pucc»eout); return 0; *2 Der beste Weg. um nkht mit der Funktion COUt ( ) zu kollidieren ist es. innerhalb der Funktion das Objekt cout lokal i.’inzub m-drn SofiMgrt das Objekt Std: : COUt nur innerhalb von { } Gleiches wurde h*er auch mit Std: :tndl gemacht Hier hattest du zwar auch den kompletten Namensraum Std einblenden können, was aber unnötig gewesen wäre. *4 Für String habe ich mich auch hier für den Namensbereich mit ZugrifHoperator । ntschicden (std:;string- Wenn String noch häufiger verwendet worden wäre, hättest du auch den kompletten Mimen global über using std:: st ring; einHenden können. iSelobnung/Löswgl Jetzt Ist Halbzeit! Zwei umfangreichere Themen haben wir hier noch, und dann darfst du in die OOP einsteigen. Noch eine gute Nachricht: Heute ist Schnitzel-mit-Pomme$-Tag Lasses dir schmecken* 264 CtPifl ACHT
Bevor dein Compiler den Quelkode zum Arbeiten bekommt, wird vom Präprozessor noch eine Quelhextersetzung durchgeführt. In der Regel kriegst du diese Ausführung des Preprozessors nicht mH, weil sie automatisch vor dem Compilerlauf ausgeftihn wird. Diese textuclle . Vorübersetzung * (prä=vor) fühlt sich für alle Zeilen in deinem Quelltext verantwortlich, die mit dem Lattenkreuz-Zeichen # beginnen (auch Direkti- ven genannt). Eine solche Zeile braucht nicht mit einem Semikolon abgeschlossen zu werden. Pro Zeile ist ein Lattenkreuz Befehl erlaubt. Bevor Compiler und Linker ubtfhjupi rtwis wbrn. dirf dw Priptfeftfiftr der» «jbfwbeiitf*. #include Ja, genau, und wenn wir schon dabei sind ... Sch] ^w;uprl;»r fUr Typ»*-. h'jiymHrtlch» die Mproim$r 255
linclude kennst du Ja eigentlich schon aus allen vorherigen Programmen. Dieser Präprozessorbctehl kopiert in dein Programm den Quelkode einer Header- oder Quelldatei ein. Hierbei stehen dir folgende zwei Geschmacksrichtungen *1 Bri den Klammern <> «vifd in einem voreingestellten Pfad nach der Datei vanille gesucht. Die Hcade/dateven deine* Standardtibl^o- ihek, wie bspw. <iostrcam>, <string> oder <vector> werden ja auch auf diele Weise eingebunden. zur Verfügung: linclude <vanille> *1 linclude "schoko1 Hier wird zunächst Im lokalen Verzeichnis nach der Datei Schoko geiixht n item du dir Qurlldatfi gespeichert havt, wr’rhe dir Zeile linclude "schoko” enthalt , Wird dort Schoko nicht gefunden, wird nochmal ur voreingestellten Pfad als stunde geschrieben <schoko>) nach der Datei gesucht. Die Form mit den doppelten Gänsefüßchen oben verwindest du gewöhnlich Ak deine eigenen Headerdateien (Hintrrgrundin^l Der voreingestellte Pfad wird in der Regel von deinem Compiler oder der Entwicklungsumgebung festgelegt. In der Praxis kannst du hierbei weitere Pfade hinzufügen Wie das geht, hängt natürlich von der verwendeten Ent- wicklungsumgebung oder dem Compiler ab Hierfür kannst du auch den relativen oder absoluten Pfad zwischen den Gänsefüßchen angeben. So geht’s auch: linclude "includc/crdbccre^h" linclude "/hotre / d le cer/baer/gaensefues sehen/ ine lüde/vanille .h" linclude ,r.. /ganzwoanders/include/waldmeister.h" A5it ffdefine lässt du Text vom Präprozessor durch einen anderen Text ersetzen. Vergiss nicht, für den Präprozessor ist alles ein Queilcode-Text! Der Präprozessor kann kein C++ und kennt auch nicht dessen Syntax! Hierbei kannst du einfache Symbole verwenden, die vom Präprozessor dann durch etwas anderes ersetzt werden. Auch lässt sich die Ersetzung von tfdeflne missbrauchen, um echte Makros zu erstellen. [Hintergrundintal Ein Makro ist ein Stückchen Code, t der durch ein Kürzel vereinfacht wird und vöm __ 'M Praprozessor spa- • • *•" * *- . f r an allen Stellen ersetzt wird, wo T 11 I das Kürzel steht! 265 ACHT
Hierzu ein solch einfaches #define-Symbol: *1 Das Symbol hier SCHROEDINGERS KATZE -- Ob^rdl «m Quelltext, wo du jetzt fdefine SCHROEDING£RS_KATZE "Garfield" *1 . **"- " diwesymbolische Konstante verwendest, __ wird vom Präprozessor d»e konstante Zeitheftkette ^Garfield* cout « SCHROEDINGERS KATZE « endl; *2 < ersetzt cout « "SCHROEDINGERS KATZE” « endl; •J Steht dw Symbol zwischen einer Zckhrnketten- konstantc. wird vom PfljKümsor keine Erset- zung durchgeführt 2 Das Symbol SCHROEDINGERS_KATZE wird nach dem PripforesvorUuf durch nGarfleldn ersetzt, weshalb dann auch Garfield ausgegeben wird Macht nichts! Anstatt jetzt im gesamten Quell text den Namen zu ändern, musst du hier Ja nur den Wen des tfdefine- Symbols SCHROEDINGERS_KATZE ändern. "1 Hier bekam.’nT Makro Iiko formalen Parameter, welcher in Klammern stehen muss l Diesen formalen Parameter kannst Liu au# der rechten Seile beliebig oft LverweMen. Dort muss dieser aber Buuch zwischen Klammern stehen! *ÄjiliLh kannst du auch hier mehrere nwrer verwrndei^^fl Jetzt noch ein Makro mit #define idefine QUADRATIEREN(val) ( (val)*(val) ) »1 int quadrat QUADRATIEREN(10); *2 Solche Makros können naiürhch wesentlich komplexer und umfangreicher ausfallen und über mehrere Zeiten gehen. In solch einem Fall musst du am Zeilenende ein Backslash Zeichen \ anhüngen. ’S im Quelltext w»rd an jede< Steffl lieses Makro vum Präprozessor dann dc den t-igcnUicben Code ersetzt Au-. QUADRATIEREN ( 10) macht der l Pr.iprozeswr .10’10". Das »st danr^uiii das ‘.-. di OW Compiler knett lAchtun^l Jdefine-Symbole und -Makros werden in C++ eigentlich eher selten für funktionelle Sachen verwendet, weil diese doch oft Nebenwirkungen haben, auf die dich kein Arzt oder Apotheker hinweist. Wir gehen in der Werkstatt naher darauf ein. Die bessere Alternative für fdefine SCHROEDINGERS_KATZE wäre demnach static const string SCHROEDINGERS—KATZE ~ und für das Makro QUADRIEREN wäre inli- ne int QUADRIEREN(int val) {„} (oder eine Lambda-Funktion) eine sinnvollere C++-Variante. Wenn du ein Symbol oder Makro aufheben willst, kannst du dies dem Präprozessor mit tfundef und dem Namen miHeilen. Sch] Üiif-Ltoort »r fUr Typefii N,j » n « j c h » urg pj» > >| r m > v»r 257
Zum Beispiel: lundef SCHROED1NGERS_KATZE Wenn du diese Zeile im Programm verwendest, werden hinter dieser Zeile alle Symbole oder Makros mit dem Namen SCHROEDINGERS_KATZE ungültig. In der Praxis wirst du aber nicht zum Spaß ein Symbol oder Makro auflieben, um dir dann die Fehlermeldung des Compilers anzusehen, son- dern eher, um anschließend einen neuen Wert mit tfdefine für SCHROEDINGERS KATZE zu setzen. Recht nützlich Ist auch bedingte Kompilierung Das bedeutet, du lasst den Preprozes- sor bestimmen, welchen Code der Compiler zu sehen bekommt und welchen nicht. Damit kannst du praktisch einen Code zur Verfügung stellen, mit dem Birnen-Jünger, Mikroweich-Anwender und Pinguin-Verliebte zufrieden sind, Theoretisch zumindest! Da es recht viele Direktiven dafür gibt, hier alles in einer kompakten Tabelle: | Direktive | Was sie bringt Hf AUSDRUCK kl der AUSDRUCK, ungleich 0, bekommt der Compiler den dahinterstehenden Progummteil iu Gesicht. Hfdcf SYMBOL Hf deflned SYMBOL Hf deflned (SYMBOL» Ist SYMBOL definiert (bspw. mit tfdc/anc), ethllt der Compiler den darauf folgenden Programmiert, /ifndef SYMBOL iif idefinied SYMBOL Hf !deflned(SYMBOL) Ist SYMBOL nicht definiert (bspw. mit # de fine) erhält der Compiler den darauf folgenden Programmiert. Hilf SYMBOL tfelif AUSDRUCK Ist dis SYMBOL definiert oder der AUSDRUCK ungleich 0. wird vom Compiler der darauf folgende Programmteil verwendet. Einem # elif geht natürlich ein #if oder fii fdef voraus. #else Den alternativen Programmiert hinter dem fielse bekommt der Compiler vorgeseia, wenn alle vorangehenden iif def usw, nkht lutreffend waren, Hndif Damit zeigst du dem Praprozessor das Ende seiner bedingten Kompilierung an.. 268 acht
#lch bestimme, was #du bekommst In diesem Abschnitt will ich verstärkt auf linclude eingehen, weil es eben das ist. wovon du künftig als Entwickler in deiner Schuhfabrik recht rege Gebrauch machen wirst und musst. Also, aufgeht’s zu den hohen Lehren von tfinclude! Einmal einfügen reicht Wenn du eine eigene Headerdatei erstellst, musst du Vorkehrungen treffen, damit diese Datei vom Präprozrssor nicht mehrfach in unterschiedliche Dateien eingefügt wird, welche diesen Header ebenfalls inkludieren. Die Formulierung für den Präprozessor sollte somit lauten: Hast du den Inhalt der Headerdatei noch nicht eingefügt, dann mach zu, ansonsten hast du hier nichts zu suchen! Das Ganze (etwas sanfter) als Code: ff Schroedingcr.h fifdef SCHROEDINGER_H 'M »#define SCHROEDINGER_H *2 // Hier komme der Code für tfendif *2 War da* Symbol SCHROEDINGER_H noch nicht vorhanden dann wird cs Jetzt fcstgelcgt DiescZede bekommt der Präprozessor nur einmal zu Gesicht Für deinen Prä- prozessor bedeute' diese Zei e c!j% Ende der bedingten Koni H»er kommt dec Code h. n. der vom Praprozessof für den Compiler in die Date-» welche den Herder SchroedInger.h inkludiert hat |Fchl«r>,MuB(| Wenn bei vielen Headerdateien und Modulen etwas mehrfach inkludiert genrsmd Ze<l jjetit d.r Arbrlt P(i|Morcison tert hin tfendif weiter Jetzt ein „halbwertiges“ Beispiel dazu. Die Headerdatei: wird, können Makros der Headerdatei mehrfach definiert werden und können somit einen Fehler auslösen. Wobei II Schroedinger,h finclude <iostrearn> linclude <string> using namespace std; fifndef SCHROED1NGER_H du in O* ohnehin am besten die Finger von A^akros lässt? Sch](|j$»Lnört|f für Typ#«, kj#nib#r»ich• U«d di# Pr*pr o i * s »r • >1 r • k t & v#n 259
Idefinc SCHROEDINGER H struct Schroedingera_Katze { string name; unsigned int alter; >; lendif Diese Headerdatei kannst du jetzt ganz einfach in deinem Programm verwenden: ' 1 Den Header brauchst du lediglich nvitdien Gänsefüßchen hmruzufugcn. Hier wird davon jusgcgangcn, dass sich die Hc.idcvd.ite« Schroedinger .h irr selben Verzeichnis befindet .vic Hauptprogramm.epp1 II Hauptprogran».epp linclude <iostreaen> linclude "SchroedInger.h" *1 using namespace std; '? d>e Suukiur aus der Headerdatei int main() Schroedinger.h Schroedingers_Katze kater = { "Felix", 9 > .No such flle or dlrectory“, oder wo bin Ich hier... Wer diesen Fehler lesen und verstehen kann, isi ganz klar im Vorteil, Diese Fehlermeldung bekommst du von deinem Präprozessor zurück, wenn er die Headerdatei nicht finden kann. Für dich bedeutet das, dass du den Pfad zur Headerdatei falsch angegeben hast. Also deine Schuld und nicht meine! lEinfjchr Aulgibe] Guck dir das folgende Bild an. Im Hauptprogramm main.epp fehlen da noch die Header- dateien a.h, b.h und c.h. Wie müssen die drei linclude- Zeilen aussehen, damit es kein .No such file or directory' gibt? Achte auf die Verzeichnisse und Hierarchien von A. 8 und C! Wie mvnen die lincludC1; in mnin.cpp ru< A.h, b.h vnd C ih 260 Gplt»! ACHT
Hier die Lösung der drei #include’s: linclude •».JA/a.h" linclude "b.h" linclude "C/c.h" und *2 ... durch die [«dlolge bzw. dem Wert 3 - 1415 cr$eUt. Makros und Symbole, oder doch lieber nicht? In C*+ seihest du. wenn möglich. auf symbolische Konstanten und Makros verzichten, weil die negativen Nebenwirkungen oft auf den ersten Blick gar nicht erkannt bemerkt werden. Gerade Makros sind da eine unerschöpfliche und geduldige 1 quelle, welche den einen oder anderen Programmierer schon zum Arbeitsamt | haben. Hier eine solche symbolische Konstante im Ein: fdefine PI 3.1415 ’1 *1 Diese sy/ntwüsctu' Konstante PI wird . . , . , .. vom P<lMores«M double durchmesset 5.22; M I double kreisumfang durchmesser * PI; "2^- double kreisflacchc durchmesser*durchmesser*PI/4; Willst du PI gleich errechnen lassen, kannst du dies mir #define auch so m linclude <cmath> // für atan() fdefine PI atan(l)*4 An dieser Stelle solltest du allerdings wissen, dass ja der Preprozessor nur eine textuelle Ersetzung (kein C++, gell!) vernimmt und den Text für jedes PI direkt im Programm ablegt. Das bedeutet, dass jedes PI erneut im Speicher abgelegt wird, und das bedeutet im zweite Fall mit atan( ), dass jede Berechnung erneut an Ort und Stelle ausfiihrt werden muss. Tataaa, diese Nachteile hast du mit const nicht: static const double PI 3.1415; II oder als berechneten Wert: static const double PI = atan(l)*4; Der Vorteil von const gegenüber tfdefine ist also, dass nur einmal ein Speicher- platz zugewicsen werden muss, und auch, im Fall von atan( ). die Berechnung nur einmal bei der Zuweisung ausgefuhrt werden müsste. Zusätzlich kannst du, dank fester Spelcherstelle. auch mit einem Zeiger auf const zugreifen (theoretisch zumindest). Sch] UiflirOrt«*- für TypTii und di» Prlpr »irek 11 «1
Querschläger Makros... Ja, gut. bei symbolischen Konstanten wirst du dir noch sagen: .Mensch, so schlimm sind die auch wieder nicht!' Aber Makros kannst du nicht mehr schönreden. Die sind böse (’viel-Weihwasser-spriiz*). Also nicht verwenden! Hierzu eine der möglichen Nebenwirkungen, die Makros gerne mit sich bringen: Idefine QUADRATIEREN(val) ( val * val ) int wert 10; int quadratl QUADRATIERENfwert); int quadrat2 - QUADRATIEREN(wert+1); II 11I cout « quadratl « endl; cout « quadrat2 « endl; II El! Der Code bei der Ausführung A o __________ UUio-HOJI» Dl E T E R BAER 1 O O . f MAKROHELL Da is1 w« schiefjefjuicn??? Das hier .21’ hcrausgekommen ist. liegt daran, dass aus der Ersetzung von (val * val) eben (10+1 * 10+1) wird. Nach der Punkt-vor-Strich-Regelung wird hierbei eben 10+ ( 1* 10) + 1 gerechnet. Ganz klar, hier wurden die Klammerungen bei dem Makro ver- gessen ( (val) * (val)). Aber solche Beispiele sind es leider, die immer wieder für einen Riesenwirbel sorgen. Nein, natürlich nicht komplett. Gerade für Dinge wie die bedingte Kompi- lierung oder zu Debugging Zwecken sind solche # de fine-Direktiven nach wie vor nützlich. Aherals Alternative für Makros sind inline-Funkiionen und Funktions- Templates die wesentlich besseren C+r-Player. Und stall symbolischer Konstanten wür- de Ich dir außerdem konstante Variablen mit const empfehlen. #define S 262 ACHT
«Ich h««« all«« v##«ch«##t Sor nun weißt du, dass bevor dein Compiler ins Spiel kommt, vom Präprozessor die Lactenkreuze verschluckt und der Quellcode in einer endgültigen Fassung für den Compiler/übereilet wird. Headerdateien (auch Quelldateien} werden mit tfinclude einkopiert. #define Symbole und Makros durch den entsprechenden Text ersetzt und die bedingte Kompilierung wird mit #if. tfifndef usw. durchgefühn. {Schwierige Aufgabe! Nachdem du schon alle drei Systeme hochgefahren hast, kannst du gleich ein Beispiel mit einer bedingten Kompi- lierung erstellen, welches ermittelt, auf welchem System das Programm gerade ausgeführt wird. Bekannte Symbole sind ___APPLE___(für MacOS), linux (für Linux) und WIN32___(für Windows) Gib einfach das entsprechende System oder den Boss (CEO) davon aus. |ZeU«IJ Neben den von dir steuerbaren Latten- kreuz-Direktiven führt der Präprozessor noch ein paar interne Dinge durch, wie bspw. Kommentare durch ein Leer- zeichen ersetzen etc. Dass dieses fdefine-Zeug wie Makros und symbolische Konstanten nicht un- bedingt der Dotter vom C++-Ei ist, dürfte dir mittlerweile klar geworden sein. Ich hoffe es zumindest! Ich habe dich ja gewarnt, und du bist alt genug und für dich selbst verantwortlich! Mach mich später nicht verantwortlich, wenn es scheppert! '1 Eikcnml der Pn1pr<we-isor da* SYA^BOL _____APPLE________Luft da* System auf Hier eine mögliche Muster Losung : < Mac und .tim Cook-ist der CEO t*. rt. pb® v (Chief Executive Office/) linclude <lostreatn> using namespace std llfdef __APPLE__ *1»** Idefine CEO "Tim Cook" _ lelif _llnux_ <2> j| "2 Beim SYMBOL _____linux_____bekommt der Compiler .Linus TörvaMs“ für d.H Symbol CEO Zwar gibt e* bei Linux keinen direkten CEO, diher habe ich einfach den Erfinder verwendet idefine CEO "Linus.Torvalds" Icllf _WIN32 Ä 1 Idefine leise {4} Idefine lendif _ *3? V’ QV "Steve Ballmer" CEO "Schroedlnger 4 Findet der Präprozesscv kein passendes Symbol, machen wir zum Schluss (lelsci einfach ‘3 Das SYMBOL WIN32 macht .Steve Ballmer" zum neuen Symbol von CEO .Schroedlnger" zum CEO Sch]Ü*S-*L*ört«r fQr Typen, « n jfcer »i c h • und oi» Pr^pr 0 z • s bqt > >1 r • k C i v«-n 2S3
int mainO < cout « "Chef auf deinem System : " « CEO « endl; return 0; } 5 Hier wird der entsprechet CEO lut deinem System ausgeijetien Ein Bezug lut Realität war hierbei richt beabsichtigt. Etwaige Rimi lUUbezuge in der Aneinan derrcihung von Zeichen vnd rem rulallw INotieren/UI Beachte bitte, dass die Compiler- oder systemspezifischen Symbole (wie_____APPLE____.___linux_____. usw.) nicht Teil vom C++-Ständard sind und daher auch wirklich Compiler- und systemabhängig sind! Es fehlen noch ein paar Präprozessor-Direktiven, denen du gelegentlich begegnen könntest und bei denen es nicht schaden kann, diese auch zu kennen ... Direktive Was sie macht Terror "meldung” Damit lasst sich de< Quelkode nicht übersetzen und wird mit der Fehlermeldung f,meldungM abgebrochen. Kann nützlich sein, wenn mehrere Leute am Code arbeiten und du auf etwas hinweisen willst, was absolut nicht geht. 01 ine n '’datci'1 Damit kannst du die Zeilen in deinem Programm auf 11 und/oder den Dateinamen auf ”datei’p setzen. Diese Direktiven haben nur einen Einfhrss auf die Symbole _LINE_ und _FILE_1 iPpragtna Das sind compileriprzHische Direktiven. Kennt ein Compiler eine spezielle rfpragma-Direktive nicht, wird diese ignoriert. Damit kannst du Direktiven verwenden, ohne mit anderen Compilern in Konflikt zu geraten. Auch einige vordefinierte C++-Symbole sollten dir für die Zukunft noch vorgestellt werden: 1 Symbol 1 Was cs enthält LINE Dann ist die aktuelle Zeilennummer des Quellcodes enthalten. FILE «In* Zeichenkelte mit dem Namen der aktuellen Datei _DATE eine Zerchenkelte mit dem aktuellen Datum _TIHE_ eine Zerchenkelte mit der aktuellen Uhrzeit 284 ACHT
Nachdem du jetzt alle Zutaten für den Teig kennst, kannst du den Kuchen backen. Mittlerweile dürfte es bei dir schon angekommen sein, dass umfangreiche Programme nicht in eine Quell- oder Headerdatei gepresst werden, Fertigkuchen aus der Verpa- ckung schmeckt schließlich auch nicht so gut wie einer mit frischen und vielen leckeren Zutaten. Gewöhnlich werden die Zutaten in mehrere Quell- und Headerdateien ge- steckt. Diese einzelnen Bausteine werden auch Module genannt. Viele solcher Module werden am Ende zu einer Einheit, dem auszuführenden Programm, zusammengefügt. Zwar gibt es kein Dogma, nach dem du die Module auftcilen musst, aber es gibt doch einige Gebote. Und wenn du dich ein wenig an diese Richtlinien hältst, kommst du eher in den C++-Himmel und musst keine Angst haben, dass cs dir an den Kragen geht.
Für einen feineren Geschmack wird zunächst empfohlen, ein Modul in zwei Teile aufzutcilen: ** Dies wäre mit der Headerdatei (gewöhnlich mit der Endung *. hi ein öffentlicher Teil, der dir die Schnittstellen für Funktionen und Klassen an bietet - also im Prinzip sämtliche öffentliche Deklarationen. Die eigentliche Implementation (also der arbeitende Code) der Schnittstellen und Klassen, nämlich die Definitionen, findest du in einer privaten Datei. Dies kann entweder die Quelldalci (mit der Endung *. epp), eine vorübersetzte Objektdalci (Endung: * »O oder *»ob j) oder eine Biblio- theksdatei sein. Die CrvndiuUten far e^»rn IHoWendich) gute* Geichmiek 28» UOtel ACHT
*• Und natürlich ist da noch eine Quelldatei mit der Haupt- funktion main( ), welche die Schnittstellen und Klassen verwendet und ohne die es keinen fertigen Kuchen gibt. Die Zutaten sollten aber schon in der Schüssel sein, damit es mit dem Teig klappt. Einfach nur die Headerdatel hinzufügen entspricht In etwa dem Einkäufen der Zutaten. Daher ist es unerlässlich, dass du dein Compiler mitteilst, welche Zutaten (bspw. Quelldateien) zum Projekt gehören. Analog gilt dies natürlich auch, wenn nur eine Objekt- oder Bibliotheksdatei dabei ist! Diese Datei ist ja die eigentliche Zutat, welche mit in die Rührschüssel muss. Wil Al Irl in di* Rührte hültfl gthdal., rriiriJl du schon auch reintwnü! Jein! Theoretisch könntest du das natürlich schon wie folgt machen: linclude <io$cream> Jinclude "a.cpp" *1 int main() < •1 Es ist theoretisch auch mögl+ch. eine Quelldatei in d.w Projekt ;u inkludieren Aber bei graften Projekten v.»rd dici so nie verwendet. Auftcfdem selzt d.u voraus, dass immer die HiMderdale* und die Quchd.itel vorhanden ist letzteres ist allerdings nicht immer der fall’ > Sch] Ul-1-oLuort »r pur Typ + n-i h.j » n » j c h » unp qjt q g » upr - > j r q ir t fr v»r 287
Eine Headerdatei (Endung: *.h oder manchmal auch *.hpp) Ist also ftn die Vorbereitung immens wichtig. Damit holst du dir schon mal die Zutaten vom Supermarkt. Bevorzugt findest du in einer Headerdatei die Deklarationen, aber manchmal auch einige Definitionen. Hierzu eine kleine Einkaufsliste, was viele GourmetkCKhe in diesen Header stecken, damit was Gutes dabei herauskommt: Einkaufslist« Das ist gemeint Aufzählung enum pos { oben, unten, mitte }; bedingte Kompilierung fifdef _SYMBOL_ Bezeichner deklarieren dass EinKlasse; Funktion deklarieren int funktion( int, int ); inc Lude -Abweisungen fincLude <hcadcrdatci> finclude "headerdatei*1 inl ine-Funktionen inline int foo(lnt v) { return v*2; } Kommentare // Huhu, sag, was ich mache Konstanten definieren static const int PI = 3.1415; Symbole, Makros #define WORD int Nammsbereiche namespace Huhu (int h, u, h, u}; Templates definieren template<class x> dass Z{_); Templates deklarieren template<class x> dass Z; Typdefinibon struct foo{ int bl; foo* next;}; ne nützliche Einkaufsliste, was alles so einer Headerdatei verwendet werden könnte/ ilite/muss... Hierzu eine solche Headerdatei Schroedlnger.h: // Schroedlnger.h linclude <lostrcara> 288 Captf 1 ACHT
fifndef SCHROEDINGER_H fdefine SCHROEDINGER H void foo(); static const int PI • 3-1415; lendif In dieser Headerdatei findest du z. B. eine Funktionsdeklaratlon und eine Konstantcndcfinition. Den tfifdef -Teil für die bedingte Kompilierung kennst du Ja bereits. Die eigentliche Implementation der Schnittstellen von Funktionen, Klassen, Elemencfiinktionen usw. wird in einer privaten Quelldatei vorgenommen. Hier wiegen wir also unsere Zutaten für den fertigen Teig ab, so dass diese fertig für die Verwendung sind. Als Namen für diese Datei verwendet man in der Regel denselben Namen wie für die dazugehörende Headerdatei. Nur die Endung lautet hier natürlich *. epp. |HiAKti*£ruridinfot Gewöhnlich werden hier auch anonyme Namensbereiche und exportierte Template-Definitionen reingeschrieben. Eine passende private Quelldatei Schroedinger.epp dazu: linclude "Schroedinger.h’1 void foo() { cout « "Ausgabe von foo(): “ « PI « endl; Hier findest du gleich die Funktionsdellnition von foo( ) . Auch die Konstante PI aus der Headerdatei Sehr Oeding er. h wird hier verwendet. Die Hcaderdalei Schroedinger.h selbst brauchst du hier natürlich auch. lAbUf«] An dieser Stelle sei gesagt, dass du aus Schroedinger.epp und Schroedinger.h bereits eine Objektdatei oder Bibliotheksdatei machen kannst. Dafür brauchst du keine mainf)-Funktion. Um bspw. eine Objektdatei zu erstellen, brauchst du nur Schroedinger .epp und Schroedinger.h zu kompilieren, aber nicht linken! Jetzt kannst du die Objektdatei Schroedinger.o und die Headerdatei
Schroedlnger.h weitergeben Den Quellcode Schroedlnger.epp gibst du nicht frei, er bleibt verborgen. Beim Übersetzen eines Programms musst du natürlich die Headerdatei Schroedlnger. h einbinden und dem Linker mitteilen, wo er die Objektdatei findet. Ähnlich (etwas anders) kannst du übrigens auch eine Bibliotheks- datei erstellen. Beachte allerdings, dass eine Objektdatei bzw. Bibliotheksdatei nicht mehr systemunabhängig ist! Eine Objektdatei, die du auf dem Mac erstellt hast, kannst du nicht unter Windows weitergeben und verwenden. Um jetzt den Code der Quelldatei (oder Objekt- bzw. Bibliolhcksdatci) und der lleaderdatei (oder genauer, die Module) wie Zutaten zu einem Teig zu kneten, brauchst du im Prinzip nur noch eine weitere Quelldatei <* . epp) mit einer maln( ) Funktion (das Knclgerät. wenn du so willst). Der Code mit einer main() -Funktion: linclude <loscream* L ► using namespace std; linclude "Schroedlnger.h" int rnainO < fooOt return 0; J _____ u*u*n Das wahre Leben: So einfach, wie du es hier vorfindest, ist es natürlich (wie immer) im wahren Leben nicht. Wir sind hier ja nicht in einem Märchen, wo es immer zuni Happy-End kommt. Bei umfangreichen Projekten kommen schon mal ein gutes Dutzend Module zusammen. Häufig kommen noch einige Objekt- und ßibliotheksdateien hinzu. So eine Projektverwaltung ist keine Zuckerwatte, und du solltest dich unbedingt damit befassen (nicht nur mit dem Kopf nicken, ja!). Große Entwicklungsumgebungen bieten dir hierfür meistens komplette Lösungen an. 270 *cxt
Rein Ins Vergnügen An sich ist das Thema der Projektverwaltung nicht viel schwerer als Kuchen backen. Fehlt da eine Zutat, wird der Kuchen ja auch nix. An dieser Stelle kann ich dir auch den Tipp geben: Höre deinem Compiler zu wie deiner/m besten Freund/In und nicht wie deiner/m Lcbcnspartner/in! Meistens sagt dir dein Compiler schon, was falsch ist. Natürlich sind die Ausgaben von Compiler zu Compiler unterschiedlich (bzw. von der Entwickln ngsumgebung abhängig). Aber trotzdem solltest du die grundlegenden Fehlermeldungen zumindest erkennen können. Daher einige Aufgaben dazu. Itinfiche Autfgjbe) Dein Compiler meldet: „Fatal error: xyz.h: No such fi le or directory" oder .Fehler: xyz.h: Keine solche Datei oder Verzeich- nis vorhanden". Was könnte die Ursache für den Fehler sein? «SS w/ linclude Völlig richtig! Der Fehler ist plausibel, kann einen aber in den Wahnsinn treiben, wenn man ein Brett vorm Kopf hat. Entweder stimmt der Pfad zu xy z. h nicht, oder xy Z . h gibt es gar nicht, oder cs wurde vielleicht falsch geschrieben! (CirtUih» Autgib*] Einen zweiten K tus der FAQ-Sektion habe ich noch, der vom linker z geben wird. Und zwar die Meldung .Undefined Symbols: .abc()M. reference from main in xyz.o! Id: symbol(s) not found". Was könnte damit gemeint sein? Okay, ich nehme dich an die Hand! Dein Linker konnte hier das Symbol abc ( ) nicht auflösen. Das kann hier bedeuten, dass abc ( ) zwar Irgendwo deklariert wurde, dass aber der l inker die Definition nicht finden konnte und somit das Symbol nicht aufgelöst hat. Der Fehler tritt meistens auf, wenn du deinem Projekt nicht mitteilst, wo es die Definition (also die entsprechende Datei, wo sie drin ist) findet. Hierbei kann es sich um eine Quelldarei. eine Objekrdatei oder aber auch um eine Bibliotheksdatei handeln. Bei Entwicklungsumgebungen musst du diese Dateien gewöhnlich lediglich zum Projekt hinzufügen bzw. hinzulinken. Schlau.» twört »« fUr Typtn, *4 « n »1 c h » un0 di« P«-*pr o I» J »or >i r » k t 271
Natürlich darf ein Praxisbeispiel dazu nicht fehlen: H kuchenbacken.cpp linclude «iostreatn* using namespace std; namespace Zutaten < unsigned short butter; unsigned short zucker; unsigned short eier; unsigned short mehl; unsigned short backpulver; } void ruehren() { cout « "Teig wird gerührten”; } void backenf) < cout « "Teig wird gebackenen”; > void essend { cout « "Mmra, lecker, der Sandkuchenln"; > int mainO < using namespace Zutaten; butter • 250; zucker = 250; eier - 5; mehl - 500; backpulver 1; ruehrenO; backenO; essenQ; return 0; > 272 cjotf i *cht
Itinfjchr Aufgabe] Im Beispiel kuchenbacken.epp wurde alles in eine Quelldatei geschrieben. Zeig, was du gelernt hast, und teil den Code in eine Headerdatei und eine private Datei auf. Enttäusch mich nicht! Gib alles! Also zunächst die öffentliche Schnittstelle mit der Headerdatei backen, d.h.: II backen.h llfndef BACKEN *1 # de fine BACKEN ’1 “1 bedingte Kompilierung namespace Zutaten { *2 unsigned unsigned unsigned unsigned unsigned > ’2 short short short short short butter; Zucker; eier; mehl; backpulver; ’2 Definition des Nawmbereithes Zutaten void ruehrenO; *3 void backenO; *3 void e$sen(); *3 lendif *4 *3 Oekl.ir.itionen van Funktionen *4 Ende der bedingten Kompilierung Jetzt die private Datei backen. epp: If backen.epp linclude <fostream> using namespace std: void ruehrenO *1 < cout « "Teig wird gerührtl\n"; .. ° *1 Definitionen } der Funktionen void backenO ‘1 { cout « "Teig wird gebackenen"; > Schl<U3.+ lwQrt tr für Typffl. *4 » n ifr+r »j c h » und di» Prlpr >1 273
void essen() { cout « "Mmn, lecker, der SandkuchenIn"; } Zum Schluss noch die Quelldatei mit der Hauptfunktion, welche die öffentliche Schnittstelle und die private Datei verwendet: II main.epp linclude «iostream* using namespace sed; 1 linclude "backen, h' int mainO < using namespace butter = 250; zucker eier mehl backpulver ruehrenO ; backen(); essen() ; retum 0; Zutaten; i Dirw Headerdatei muss unbedingt relnl Außerdem musst du deinen» Projekt auch die QueiMatei backen ♦ epp mitteilcn! - 250 - 5; - 500 Es kann sein, dass dir das am Anfang etwas erdrückend vorkoinint, aber es lohnt sich, dranzubleiben. Sobald du eine einzige Quelldarei mit > 1.000 Zeilen hast und etwas ändern willst, wirst du an mich denken. Wichtig ist natürlich auch, dass du deinen Code in sinnvolle Module aufteilst. So macht es wenig Sinn. Kuchen backen und Wäsche waschen im selben Modul zu verwenden. Das würde ich trennen. Je besser du deinen Code in sinnvolle Module aufteilst, umso nützlicher kann dir das bei anderen Projekten sein, bei denen du diese Module auch wieder verwenden kannst, Also, nicht aufgeben und: Übung macht einen besseren Programmierer aus dir! 274 ACHT
Meister der Quelldateien Jetzt kennst du auch die Grundlagen, um einen Kuchen zu backen. Üben musst du natur lieh selbst, weil dieser Arbeitsstil ritt auch von der Umgebung abhängl, in der du deine Kuchen backst. Du weißt jetzt, dass die einzelnen Zutaten in einzelne Module mit einer öffentlichen Schnittstelle und einem privaten Teil erstellt werden. In der öffentlichen Schnittstelle, der Header- datei findest du vorwiegend die Deklarationen (aber auch vereinzelt Definitionen). Im privaten Teil, der als Quellcode <*.cpp). als voriibersetzte Objekt oder Bibliotheksdatei vor liegt, findest du gewöhnlich die Definition der Deklarationen der öffentlichen Schnittstelle! IW'ifrtn/Qbrnl Im Gegensatz zum privaten Teil, der ja auch als Objekt- oder Bibliotheksdatei vorliegen kann, ist der öffentliche Teil auf jeden Fall in lesbarer Form vorhanden. Wir haben es ja schon besprochen, aber es Ist sehr wichtig, dass du zwischen einer Deklaration und einer Definition unterscheiden kannst. Daher nochmal vereinfacht: »* Mil einer Deklaration machst du dein Programm mit einem Namen (Bezeichner) bekannt (denk an Pamela Anderson/Johnny Depp). Damit kaufst du die Zutaten für deinen Teig ein. Bei einer Definition wird Speicherplatz für die Daten und den Code des Namens angelegt. Dort findest du bspw, den Code für die Funktionen oder den Inhalt einer Klasse. Du wiegst praktisch deine Zutaten ab, damit diese für den Teig verwendet werden können. Wenn du also mehr als nur einen Namen verwendest (Pamela Anderson/Johnny Depp steht vor dir), handelt es sich um eine Definition. Wenn du das verstanden hast, kannst du bspw. folgenden Fehler einfacher beheben: (Schwierige 4wfg<be| Im folgenden, dir bereits bekannten Codeausschnitt backen,epp weigert sich der Compiler, eine ausführbare Datei daraus zu machen. Er behauptet doch strikt, dass wir ein doppeltes Symbol (Id: duplicatc Symbole Zutaten::butter) In den Objektdateien haben, und macht daher nicht weiter. Die dir bekannte Headerdatei backen.h aus dem Büro wurde hier nicht geändert. Kannst du den Fehler erkennen? Und warum ist es ein Fehler? Sch 1 Un.«-l»ort »<• rur Typ»n, ch, jn<3 <31 » Sr-tpr 0 I 0 3 - > 1 r g . t > v»r 27 5
Hier der Code mit den Problemen: linclude <lostrearo> linclude "backen.h" using namespace std; void ruehrenO cout « "Teig wird gerührt!\n'*; Ja richtig! Dadurch, dass hierbei der Header einkopiert wird, wird neben den Deklarationen, die Ja keine Probleme machen würden, auch die namespace-Definit Ion einkopiert. Im Hauptprogramm, wo die main ( ) Funktion enthalten ist, wird dieser Header erneut mit linclude ’r backen,h” einkopiert. Am Ende steht der Linker vor zwei gleichlautenden Namensbereichen (also doppelter Pamela Anderson/ Johnny Depp) und kann die Symbole nicht mehr auflösen. |BelohAun$/L6fUAg| So, jetzt genieß den Kuchen mit deiner Freundin und freu dich schon mal auf das nächste Kapitel. Das Warten hat endlich ein Ende! OOP kommt gleich ... 27® ACHT
—NEUN — Klassen Von Hexenmeistern, Todesrittern und Datenkapseln Jetzt wird es langsam ernst. Schrödinger Ist ja bereits mit den verschiedensten Klassen von WoW vertraut. Er selbst Ist ein Hexen- meister der Stufe 63. Allerdings merkt er recht schnell, dass die Klassen von C++wenig mit den Klassen von WoW oder mit Schulklassen gemein haben. In diesem Kapitel lernt Schrödinger die Grundlagen der Klassen und somit dor OOP kennen. Mit den Daten- kapseln lernt er das erste wichtige Konzept der OOP kennen, das auch hier wie ein Medikament In der Programmierung wirkt.
Jetzt bist du im Tal deiner Träume angelangt und lernst OOP. Dass die Abkürzung nicht für Oben-ohneProgranirnirruiig stehl, kannst du dir Ja denken. Daftir gibt es schon das Kürzel FKK(-P). OOP steht schlicht für objektorientierte Programmierung, wie du selbstverständlich weißt (oder auch nicht?). Sicherlich fragst du dich jetzt, was hinter dieser OOP steckt? Naja, ganz so krass würde ich das jetzt nicht stehen lassen. Die Idee hinter der OOP ist. dass du dort deine Objekte aus der realen Weh abbilden kannst. Und .reale* Objekte haben nun mal mehr zu bieten als die bloßen Eigenschaften, wie du sie in einer Struktur definiert hast. Viele Objekte haben auch Funk- tionalitäten. Am besten ein Beispiel hierzu: Der Hund ist (k)eine Biotonne: Attribute (Eigemchaften Typisch 00^ Nrn drn f ig*ntchi1trn (Daten} Kil Obj<k1 trltat Fifiigkr icen. welche all UNTienthjnktiMien Im Objekt selbst geschrieben werden. Rass« Exakt, in der OOP stehen die Dalenstrukturen im Mittelpunkt, die in der Regel nur von den Funktionen des Objektes behandelt werden. Ein Zugriff von außen auf die Objekte sollte dann nicht mehr möglich sein. Eine solche Datenkapsel aus Daten und Funktionen wird dann als abstrakter Datentyp bezeichnet. 278 HCUN
Programmiechnisch wird die OOP mit Klassen realisiert. Für solche Zwecke haben die O*-Entwlckler das Schlüsselwort dass in die Welt gesetzt. •i Die Klasse wird mit dem Hier eine Klasse in der Theorie: Schlüsselwort dass eingeteiiet Dahinter fojgt gleich der Name der Klasse • Hund dass Hund *1 und gültiger Bezeichner sein muss II Daten und Funktionen *2 . «3 ’3 Wie schon bei den Strukturen, muist du auch die KIwe mit einem Semikolon ihschicßcn! •2 Zwischen den geschweiften Kammern { } findest du die Eigenschaften (Daten) und Funktionen (ElemrnrfunktloFM,n> der Klawc |H»nter^rundinlo| Eine Klasse hat In der Programmierung natürlich eine etwas andere Bedeutung als in der Soziologie oder Biologie. In der Programmierung ist die Klasse eine (darauf hast du gewartet, gell?) gemeinsame Struktur und das Verhalten von Objekten in einer Software! Am Ende, wenn du alle Daten und Fähigkeiten in die Klasse gesteckt hast, die du dort unterbringen wolltest, kannst du echte Objekte davon in die Welt setzen. Eine solche Geburt wird auch als Instanz einer Klasse bezeichnet. Mehrere IrHIirufn eines Objektes, welche nir liutfreit ivt der Killte Bund eneuf t wurden K]m*n 279
Ein Objekt erblickt das Licht der Welt Bevor du überhaupt etwas in den Editor cingibst, solltest du dir vorher schon mal erste Gedanken machen, was deine Klasse so für Daten und Funktionalitäten beinhalten soll. Der größte Fehler, den du in der OOP machen kannst, ist es. einfach drauflos- zuprogrammieren. Meistens hat das zur Folge, dass hinterher das ganze OOP Prinzip aufgeweicht werden muss. Ohne Plan geht schon mal gar nichts: Df r mir Entwurf unifvfr Kluse ,Awto" mitsamt f ijenwhahf« und Methode* Das leere Grundgerüst für die Klassendefinition sieht daher wie folgt aus: dass Auto { } ; Jetzt kannst du die Mitglieder zu deiner Klasse hinzubitten. Hierbei werden wohl zuerst die Eigenschaften bzw. Attribute (= Daten) zur Klassedefinition hinzugeftigt. 280 Cecktfl MCUN
Rein mit den reinen Daten dass Auto < unsigned unsigned unsigned >1 short leistung; ‘1 short baujahr; *1 short hoechstgeschwlndigkeic; ’1 •1 Die Daten der Klam Auto Nachdem deine Klasse Auto jetzt Daten fiir die Leistung, das Baujahr und die Höchstgeschwindigkeit erhalten hat, fehlen dir nur noch die Elementfunktionen dazu. Jetzt noch die Elementfunktionen deklarieren und fertig ist das Klassengrundgerüst: // Auto.h short short short lelstung; baujahr; hocchstgeschwindigkeit; [TTTJ^TTT] ffM'i* Zwar geht cs hier noch nicht um die Initialisierung von Klassenelementen, aber du soll- test hier schon mal im Hinter- kopf behalten, dass das direkte Initialisieren von Daten einer Klasse (also Klassenelementen) nun seit dem neuen C++11- Standard möglich ist. *1 Das SIM die typischen Eiemcot- fuMttioneo. mit denen wir die Fähigkeiten der Kl.usr AutO □.nwenden können. {Code beiibt-itenj In der Praxis werden inner- halb der Klassendefinition nur die Elementfunktionen deklariert (meistens in einer Headerdatei). Die Definition erfolgt gewöhnlich in einer anderen Quelldatei (meis- tens eine private Quelldatei}. dass Auto < unsigned unsigned unsigned hupen(); bremsen(); *1 fthrenOi *1 set^leistungtunsigned short); *2 set_baujahr(unsigned short); *2 set-hoechstgeschwlndigkelt(unsigned short); *2 short *2 D.w sind ebenfalls Elementfunktionen, d«e abc* lur das Initialisieren der taten in der KUsse zuständig sind. Schließlich soUcn wir ja nicht mehr von außen auf die Daten zugteifen dürfen! void void void void void void unsigned unsigned short unsigned short >; get_leistung(); *3 gc t_bauj ahr(); *3 gec_hoechstgeschwindigkeic(); *3 "3 Weitere Elementfunktionen. welche den Inhalt voo den Daten der Klasse zurückgeben Deklaration weil die Es ist durchaus verwirrend, wenn die Rede von einer Klassendefinition ist. in der Klasse in der Regel nur deklariert und erst woanders definiert werden. ABC { } ; von der Definition einer Klasse und nicht von einer Deklaration, ein dass ABC; ohne die geschweiften Klammern angegeben wird! Definitionen der Elementfunktionen der Klasse, und du könntest die ist. musst du schon noch etwas mehr über die Flicßbandarbeit talltlt 281
£ Kontrolle: Du kommst hier nicht durch Wenn du Jetzt ein Objekt der Klasse Auto anlegen würdest, so würde dies zwar funktionieren, aber es fehlt noch der Zündschlüssel dafür. Du hättest praktisch ein Objekt, könntest aber nicht auf die Daten und Elementfunktionen zugreifen. Solch private Daten und Elementfunküonen können nur innerhalb der Klasse verwendet werden. Der Versuch, zu hupen Ohne weitere Verkett rwng en ist Alles in der Klasse von lu&en unerreichbar oder geniM* .prwile“. Diese kannst du durchaus mehrfach in deine Klasse einbauen. Zum Beispiel: u auch gleich den Unterschied en Strukturen (struct) und er Klasse (dass). Während bei Strukturen von Haus aus alle Mitglieder von außen erreichbar sind (public), sind bei Klassen alle Mitglieder zunächst einmal von außen unerreichbar (private). Damit du auch selbst die Verant- wortung übernehmen kannst, gibt es dafür die Schlüsselwörter private und public. dass KlassenNarac < private; II Auf die folgenden Mitglieder kann nur II innerhalb der Klasse zugegriffen werden public: II Auf diesen Elementen ist ein II Zugriff von außen möglich private: //Ab hier kann wieder nur innerhalb der // Klasse auf die Elemente zugegriffen werden 282 MCUN
private z’ ? Hm, wie in deinem echten Leben solltest du dein Aller nicht verraten und dich nur von deiner besten Seite zeigen, So ist es in etwa auch bei den Klassen. Daten seihen in der Regel auf keinen Fall von außen erreichbar sein und nur über die Elemenlfunk- tionen als Schnittstelle behandelt werden. Daher werden meistens nur Elementfunkti- onen in der Öffentlichkeit verwendet und als public gekennzeichnet. Aber auch hier kannst du private Elemenifunkttonen schreiben und verwenden. Mit diesem Halbwissen kannst du nun die Klassendefinition der Klasse Auto und in unserem Fall der Headerdatei Auto.h abschließen: // Auto,h fifndef _AUTO_H Idefine __AUTO_H dass Auto ( private unsigned unsigned leistung; | j short short baujahr; ’1 Em Zugriff ist ab h»cr von außen nicht mehr möglich Oa eine Klasse von Haus aus private <st. hattest du hier auch auf das Schlüsselwort verzichten können. unsigned short hoechstgeschwindigkeir; public: void hupen(); void bremsenO; void fahrcnO; '2 Alles, was ab dieser Stelle über das Objekt folgt, kann von außen verwendet weiden. void set^leistungfunsigned short - 0); void set_baujahr(unsigned short » 0); void set_hocchstgcschwindigkeit(unsigned short • 0); unsigned short gec_leistung(); unsigned short g€t_baujahr(); unsigned short get_hoechstgeschwindigkeit(); >; lendif iNotizl Bei den Elementfunktionen kannst du selbstverständlich auch die Standardwerte (Default-Werte) für die Parameter verwenden, wie du sie bereits von den Funktionen her kennst. Natürlich gelten hierfür die gleichen Regeln wie bei den Funktionen. 263
Bel Klassendefinitionen den Überblick behalten Ich habe dir bewusst noch nicht die Definitionen der Eiementfunktlonen und die Erzeugung bzw. Verwendung von Objekten aufgetischt, weil das dann doch etwas viel im kleinen Räumchen gewesen wäre. Mal schauen, ob du Oberhaupt die Klassen- definition verstanden hast. Hierzu soll mal deine Katze herhalten -Ac-te'R Z-EWW lEifrfjchr Aufgab*) Hier hast du jetzt eine Klasse deiner „Katze“ mit einigen typischen Fähig- keiten und Daten. Mach daraus bitte eine Klassen- definition! Hierzu eine sinnvolle und mögliche Musterlösung: dass Katze < private: string rasse; unsigned short gewicht; unsigned short alter; public: void fressend ; void schnurren(); void sofa_zerkraczen(); void set_rasse(string); void set_gewicht(unsigned short); void set_alter(unsigned short); 284 MCUN
string get_rasse(); unsigned short get_gewicht(); unsigned short get_alter(); > Auigjb*] Beim Übersetzen der Klasse Katze kommt es zu einer im folgenden Bild dar- gestellten Fehlermeldung Was ist hier falsch gelaufen? A <2 1 KAU* Dieter Baer $ gtt ip4.fr -Wall petUntlc r.td*<oex o Blau uin.epp Decken.h; In functlön ‘int MlAlP: backrr.h:15:lB! erroc: 'std: istrsrg Katze:irww' 15 prlvit« Bairi.cpp:12;12: error: witbin thli ccr*text b*cken.t>:l6!l8: error: '$hort unngftrt int Katie:sgwlctit' 15 private aln-cpp:13:12: error: wltbin thls cofttext backen.ti:l7;18; error; ’sbert unsionee mt Katltssalter’ B private am.epp: 14:12: error: wkthir this context Dieter Baer $ | Die Ausgabe der Fehler- meldung: das ja jf)a c6c£ Völlig richtig! Saugut! Und wenn wir schon beim Thema sind, noch eine letzte Frage: A tEhfttah* Aufgabe] Wie kannst du bei der Klassendefinition den Zugriff auf die Mitglieder der Klasse festlegen? Jetzt komm schon, das war doch leicht! Also gut. nochmal: Mit private kannst du deine Elemente der Klasse von außen schlitzen und nur noch innerhalb der Klasse dar- auf zuruckgreiten. Standardgemäß ist eine Klasse auch ohne das Schlüsselwort pri- vate. Mit dem Gegensuick ptiblic werden bestimmte Mitglieder der Klasse, was auf keinen Fall die Daten sein sollten, von außen erreichbar. {Achtung/Vorsicht Du solltest nie inVersuchung geraten, einfach aus Bequemlichkeit Daten public zu machen Das würde die Datenkapsel aufweichen und wäre auch nicht mehr im Sinne der OOP* Daher denk vorher nach, was du alles für Daten in deiner Klasse brauchst, um nicht hinterher irgendwie improvisieren zu müssen. |Be-loh»UHg/LMuag| Gönn dir doch ein wenig Pause Vielleicht mit einem heißen Bad oder einer schönen Tasse Tee auf dem Sofa. Die Definition (und noch mehr) von den Elementfunktionen beschreibe ich dir dann (frisch erholt) gleich im nächsten Abschnitt* 285
So. bis Jetzt kennst du ja quasi erst das Skelett einer C+*-Klas$c, Das. was du über das Skelett ziehen musst, ist noeh dir Definition der Elemcntfunktioncn, und über die darfst du jetzt gleich lesen. Alles, was du bisher über die Funktionen erfahren hast, gilt natürlich auch für Elcmcntfunktionrn. mit dem einzigen Unterschied, dass Elementlünktionen eben Teil einer Klasse sind. Immerschön der Reihe nach. Du könntest dir Definition der Elemcntfunktioncn innerhalb der Klassendefinition einbauen, was eher unüblich, aber trotzdem möglich ist. In der Praxis solltest du das aber eher nur bei kleinen Klassen machen. Folgendes wäre also möglich: dass Auto < privates unsigned short unsigned short unsigned short public: void hupen() { Iclstung; baujahr; hoechstgeschwindigkeit; cout « "Huuuuup!11\n } ’V nimr i.- -.-a___r ' du innerhalb einer Klasse definierst, werden implizit zu inline-Elementfunktionen (auch ohne das Schlüsselwort inline). Damit schlägst du dem Compiler vor. die Elementfunktion nicht über den JH’ntergrundlnfo] Elementfunktionen, K«W I Die Definition der Elewntfunktion einer Klasse kann auch innerhalb der Klassen* definition erfolgen. In der Praxis Ht dies allerdings eher unüblich’ Stack-Frame aufzurufen, sondern direkt im Code zu verwenden. 28» MCUN
Jetzt zur gängigeren Praxis der Definition von Elementfunktionen: Meistens wirst du deine Elementfunküonen außerhalb der Klassendefinition In einer separaten Quelldatei C.cpp) definieren wollen. In dem Fall reicht allerdings die Anga- be der Element Funktion als Bezeichner allein nicht mehr aus. Hierzu musst du zusätz- lich noch den Klassennamen mit Zugriflfsoperator : : verwenden, sonst geht gar nix! Also: Rückgabetype Kiassenname:lelementfunktion( parameter) { I/ Anweisungen > Bezogen auf die Elementfunktion hupenO der Klasse Auto: II Auto.epp « 4 linclude "Auto.h” void Auto::hupen() { cout « "Huuuuup!!!\n Explizit inline .. ch bei der Definition der Elementfunktionen außerhalb der Klasse kannst du diese mit dem Schlüsselwort inline versehen, um so dem Compiler vorzuschlagen. den Code nicht extra über einen Stack-Frame auszuführen. Damit definierst du die Elementfunktionen explizit mit inline Zum Beispiel: inline void Auto: :hupen( ) {_} Da du ja bereits bei der Definition der Elernenlfunktionen den Klassennamen + Zugriffsoperator * : verwendest, musst du eigentlich nichts Besonderes mehr machen. Du kannst dann ohne irgendwelche kryptischen Kürzel auf die privaten Daten (und natürlich gegebenenfalls auch auf andere Elcmentfunkiionen) innerhalb der Elementfunktion zugreifen. Dasselbe gilt natür- lich für explizite inline-Elementftmktionen. Zum Beispiel: 287
void Auto::set leistung(unsigned short 1) { If(1 I- 0) { leistung 1; "1 > eise { cout « "0 Leist leistung • 1; *1 •1 Im Funktipn^kopf der Elemcntfunkticn Kt bei der DeftMkxi mtAuto: : ik-r Bezug zur Klasse schon bekannt • Djibrr ist der Zugriff auf W die privaten Daten innerhalb drr Klassen über Ekmontfunktioncn kein Problem. Du darfst hier nur lesen 1 D±nk de\. yrhluwIwofUs const »ijn Efidr der tlcrnrn'twnktiofi der Elewntlunktian Nur lesen ist hier erlaubt! Elementfunktionen, welche die schreibenden Finger von den privaten Daten lassen sollen, musst du gesondert mit dem Schlüsselwort const am Ende kennzeichnen. Hier eine solche konstante Elementfunktion unsigned short Auto::get_leistung() const { *1 return leistung; > kannst du die prrw^kn Daten. hier leistung nicht Natürlich musst du die Deklaration solcher Nur lesen -Elementfunktlonen auch mit const markieren: dass Auto < public: unsigned short gct_leistung() const; II Deklaration >; 268 UHttl MtUM
Elemantf unktlonen voll Im Einsatz Nachdem du jetzt auch die Kenntnisse besitzt, Elementfunktionen zu definieren, sollst du diese Fähigkeit natürlich auch unter Beweis stellen. (Schwierig? Aafgibtl Schreib (definier) zu unserer Klasse Auto, welche du mit Auto.h definiert hast, die Elementfunktionen in einer separaten Datei Auto.epp. ’2 Das sind alles einfache Hementfunktionen. welche lediglich FahrzeuggcrAuscheauf dem GiWscturm ausgeben Hierzu eine mögliche Musterlösung II Auto.epp linclude «iastreanf» using namespace std; linclude "Auto.h void Auto::hupen() { *2 cout « "Huuuuup!!!\n"; > void Aütot threment) { cout « "Quiiiilitsch\n } *1 Ohne den notigen Header geht es natürlich gar nothl. void Auto::fahren() { cout « ,rTÖöf Tööf Brrrrurnm\n”; > void Auto::set_leistung(unsigned short 1) { lf(l 1= 0) { Leistung • 1; > eise { cout « ”0 Leistung Leistung 1; > > void Auto::set_baujahr(unsigned short bj) { if(bj >- 1900 && bj <- 2012) { baujahr bj; > > * r f *3 Das sind die S e t E lemenUirnklionefi, mit deren Hilfe die privaten Daten mrt «nnem Wert initialisiert werden können. Diese Elementfunktionen siod der einzige Weg. schreibend auf die privaten Daten zuiugreden Daher wurde hier *3 auch jeweils eine minimale .Überprüfung" auf gültige Werte durchgeführt rlllltn 219
* j Dav lind die S e t _•£ leroenUu n Schonen mit dwn Hilfe die* privaten Daten mit e»n^m Wert initialisiert werden können. Diese Elementfunktlöoen sind der einzige Weg, schreibend aut die privaten Daten /umgreifen Daher wurde hier auch fciMcik eine minimale ..Überprüfung" auf gültige Werte durchgeführt eise { cout « '•Baujahr ??? : *♦ « bj « endl; baujahr » bj; void Auto;;set_hocch$tgeschwindigkcit(unsigned short h) ("• if(h 1- 0) { hoechstgeschwindigkeit h; } eise { cout « "Höchstgeschwindigkeit - 0 ; hoechstgeschwindigkeit h; unsigncd short Auto::gct_lcistung() const { *4 return Leistung; } unsigned short Auto::get_baujahr() const { *4 return baujahr; *4 Dav Und die get. _-Elcm<-n1funM»onen. mit denen du d»e privaten Daten de* Klasse abfragen kanrtst Damit diese Etement- tiNiklKMiefi auch wirklich nur lesend auf die privaten Daten der Klwc zugreifen können, wurde diese mit const gekennzeichnet. Die gC t_- Elementlunktionen sind bei dieser Klasse der einzige Weg, um den Inhalt der privaten Daten der Klawc iw* zugeben unsigncd short Auto::gct_hocch$tgcschwindigkcit() const (*4 return hoechstgeschwindigkeit; } Dank const ist es auch nicht mehr möglich, dass du andere schreibende Elementfunktionen aufrufst, um ver- sehentlich den Inhalt der Daten zu überschreiben. Im Beispiel würdest du etwa auch eine Fehlermeldung erhal- ten, wenn du aus einer const-Elementfunktion eine set_-Elementfunktion (bspw. sct_leistung(l)) auf- rufen würdest. Innerhalb einer const-Elementfunktion kannst du also nur andere const-Elementfunktionen aufrufen. Ganz genau, aber das, also Objekte erstellen und verwenden, will Ich noch etwas ausführlicher In einem eigenen Abschnitt behandeln. 290
Toll, diese Kiessenfunktlonen Zugegeben. das Thema wird komplexer, aber bisher konntest du noch recht gut den Überblick behalten. Liegt natürlich auch daran, dass wir hier nicht mit einer Hyper geschwindigkeh durch die Welten fliegen. Fassen wir noch einmal zusammen, was du jetzt Lösung: Entweder kannst du das sofort innerhalb der K Jassen d efini- tion tun. womit die Elementfunktion als implizit inline definiert würde. » Oder, die gängigere Praxis, du tust es außerhalb der Klas- sendefinition (meistens in einer separaten Datei). Hierfür musst du dann allerdings den Klasscnnamen ♦ Zugriffs- eperator vor die Eleinentfunktion stellen (z. B. Rück- gabe Klasse: :Eleinentfunktion(para)). |finhehr Aultfjb«-] Welcher Fehler wurde im folgenden Codeausschnitt gemacht? void Auto:tsec_hyperantriebfunsigned short hyper) const { set_hoechstgeschwdndigkeit(hyper); In diesem Zusammenhang fehlt noch ein Beispiel zum Thema: Weißt du, was der Compiler dir sagen will? 291
Sieh dir folgende Codezeilen an: ff Auto.Cpp finclude "Auto.h" void print_All() const { cout « Leistung << endl; cout « baujahr « endl; cout « hoechstgeschwindigkeit « endl; > Der Compiler beschwert sich wie folgt: Dieter Baer 4 -Wall -pedant 1c -5td*<*+®K -o uknJcpp- Auto.epp Auto,epp:59:1®: error: fifictlon 'void prirt>UI I' cwmot have cv-ou>Ufler Auto.cpp: In function 'void print-AUf)*- Auto.cpp:M:ll: error: wm not decUred in thl-s iccpe Auto.cpp:®lill! error: ‘bMijaftf' was not ttcUrtd Ift tM» Auto.c«>:62:il: trrar: «ai not m »ope Dieter Baer 4 (Einfache Aufgabe) Die Funktion wurde in der Klassendefinition von Auto in Volltreffer! Das ist ein recht pcpullrer Einsteigerfehler, der schnell mal im Eifer des Gefechtes gemacht wird. VJa IBelöhflürtj/LOiüft^] Prima mitgemacht! Und so schwer war es doch bisher noch nicht, oder? Bevor du jetzt erfährst, wie du deine Klassen mit Objekten verwenden kannst, solltest du ausnahmsweise mal keine Pause machen und gleich mit dem nächsten Abschnitt fortfahren, solange das Thema noch frisch in deinem Kopf ist. 282 c«0;t«l H[UM
Nachdem du mit der Klasscndcfmition vertraue bist und die Definition der Elemenifunktionen auch nicht einen l.idschlag des Zögerns mehr bei dir auslösen, wird es Zelt, das Objekt der Begierde in den Topf zu werfen, um endlich ein laufendes Auto auf die tm Prinzip ist eine Definition der Klasse und der Elementfunktionen zunächst nichts anderes als der bloße Gedanke an ein Objekt. Wie der an das Covergirl dei- ner letzten Playboy-Ausgabe, die du in der Schublade vor deiner Freundin ver- steckst. Der alleinige Gedanke an die Schönheit und an deren Kurven macht sie noch lange nicht real! Räder zu bringen. Aufwachen, Schrödinger! Wir haben hier eine Klasse Auto und keinen Bunny fertigzustellen. Also bleib bitte am Ball! 203
Objekte auf die Welt bringen Keine Sorge, als Hebamme von Objekten hast du weniger Arbeit als Im Kreißsaal, und eine Geburt ist nicht so schwer und schmerzhaft wie im realen Leben. Die Definition gleicht im Grunde der von Strukturen: Klassenname Bezeichner; Bezogen auf deine Klas Auto alteKarre; *1 Auto oltimerl, oldtimer2; ‘2 '1 Ein neues Objekt von Auto wurde (auch als Instanz der Klasse: definiert. *2 Zwei Objekte der Klasse Auto wurden hier definiert. (Hinterirundinfo) Mit jeder Instanz einer Klasse, die du anlegst, wird auch gleich der Speicher für die (privaten) Daten bereitgestellt! Im Beispiel sind dies für jedes Objekt die Daten leistung, baujabr und hoechstgeschwindigkeit Jedes Objekt hat zwar seine eigenen Daten (eigentlich auch logisch), aber alle Objekte arbei- ten mit denselben Elementfunktionen (C+>-Gott sei Dank). Natürlich kannst damit auch ganze Vektoren oder „Bado]d-School*-ZeArrays definieren: Z*1 1OD Objekte d^r KUsie Auto jrUcgen linclude <vcctor> « « « vector<Auto> fliessbandf100) vector<Auto> prototypen; I Auto badCArray(10); •3 Thfofetocli aixti möglich s-irwd die Ze-Arrays [hier zehß Objekte dei Klasse Auto). taktisch aber besser nicht rMChmjchen* 284 HEU*
Uinjeui mit dem Objekt auf die öffentlichen Elemente der Klasse zuzugreifen, kannst du genauso wie schon bei den Strukturen den Punkteoperator verwenden: Bezeichner.Klassenelementt Bezogen auf die KlasseAuto: Auto Oldtimer; oldtimer.set_leistung(100) ' i damit -setzt du den privaten Wen leistung vo» oldtimer, was eine Insuru der Klaue Auto ist, aul den Wert 100. IZett*»] Ein direkter Zugriff auf die pr ivate-Daten ist (sollte) natürlich nach wie vor nicht möglich (sein). Daher sollte eine Verwendung wie oldtimer. Leistung» 100; zu einer Fehlermeldung führen, weil leistung private ist. Der direkte Zugriff (sowohl lesend als auch schreibend) auf diese Daten wäre nur dann möglich, wenn du diese Daten als public kennzeichnest. Aber damit würdest du den Sinn und Zweck des Klassendesigns unterlaufen Also lass den Gedanken gleich wieder fallen! Genauso lässt sich hiermit auch der indirekte Zugriff von Zeigern mit den» Pfeiloperator -> realisieren: •1 ein Auto-Ze«ge« Auto *autozeiger; *1 autozelger • new Auto; *2 autozeiger->set_leistung(100); *3 • « delete autozeiger; *4 •3 indirekter ZugriH auf tffc öffentlichen Elemente der KUsse Auto '4 den Speicher irrt Ende mit delete wie<f«rr freigeben "2 Speicher zur Laufzeit für ein Objekt Auto anfordern mithilfe des Pfeil- operators Wenn es sich vermeiden lässt, solltest du auf die dynamische Speicherreservie- rung von Objekten verzichten und statt- dessen auf die Klasse vector zurückgrei- fen. Zum einen geht es damit wesentlich einfacher, und zum anderen ist die ganze dynamische Geschichte mit new und delete einfach viel zu aufwendig und fehleranfällig' Wie dem auch sei. ich habe dich jedenfalls gewarnt! cUlltn 205
Objekte verwenden Nach dem Einstieg in die Objekte bist du nun bereit. dein erstes echtes OOP- Hauptprogramm zu erstellen. Zugege- ben, die bisherigen Mittel sind noch bescheiden und nicht ganz wasserdicht, aber jeder fängt mal klein an. Ifirttaht AirtgAb*] Erstelle ein Hauptprogramm, welches ein Objekt der Klasse Auto erstellt. Uber eine BenuUerabfrage übergibst du die Eingabe der einzelnen Werte an die set-Element- funktionen. Am Ende gibst du den Inhalt des Objektes mit den get-Elementfunktio- nen wieder aus. Hier meine Musterlösung: ‘1 Der Header, in dem die Klassen- dehniiion enthaften t$|. mu» natürlich mit rein‘ II maln.cpp finclude <iostream> linclude "Auto.h" using namespace std; Int maln(} { "2 Ein neues Objekt Auto m«i dem Be/ekhner oldtimer wird angelegt Auto oldtimer; unsigned short val; cout « “Leistung ein » val; oldtimer.sct_lcistung(val); cout « “Baujahr ein » val; oldt imer.set_baujahr(val} cout « "Höchstgeschwindigkeit ein » val; oldtimer.secjioechstgeschwindigkeictval) "3 Oie jeweiligen Daten der Klasse werden über die set-£ t’menc- funktionen m it einem Wert initialisiert cout cout cout "\nDic Eingabe lautete;\n"; “Leistung : *' oldtic&er. get_leistung() « endl “Baujahr : ** oldti&er.get_baujahr() « endl; M Hier wird der Inhalt der jeweiligen privaten Daten über die gCt-Elürnen!#unlcl ö-nen ausgegeben 298 HCUN
cout « "Höchstgesch.: ” « oldtioer.get_hoechstgeschwindlgkeit() « endl;] return 0; Damit du ein ausführbares Das Programm bei der Ausführung: "4 Hier wird der Inhalt der jeweiligen privaten Daten über die gCt-EJementfonkiionen ausgegeben Programm daraus machen kannst, musst du natürlich auch Auto.epp beim Übersetzen mit angeben. Und im Beispiel wird davon ausgegangen, dass sich die Headerdatei Auto.h ebenfalls im selben Verzeichnis wie main.cpp und Auto.epp befindet. « n (** A-'O m <xw Dieter Baer $ g+*-«i>-4.€» -Mill -pedant sc -o «rtot iiHLCff) Auto^cpq Dieter Barr | . /Mitos LclUurq : M teujetr ; 1949 Ntahstteicfwlndiftelt : Die Eingabe lautete: Leistung • 34 Baujahr ; rtö<nMgei.c»irr IW Dieter Baer S This-Zeiger Die Frage, die du stellst. über- rascht mich, ist aber berechtigt! Tatsächlich wird hierbei ein intern unsichtbarer Gegenstand verwendet. Und J zwar steht hierfür ein konstanter Zeiger auf die Adresse des jeweiligen Objektes zur Verfügung. Dieser Zeiger wird this Zeiger genannt. Sicherlich hätte man einen originelleren Namen Anden können (bspw. Schtoedinger-Zeiger)! Aber so heißt er nun mal. „Intern“ kannst du dir das so vorstellen: Klassenname* consc this iübjekt;
Zugegeben, das ist ein wenig verwirrend und erinnert eher an altägypiischc Hieroglyphen als an was Sinnvolles, daher jetzt etwas einfacher, bezogen auf die Klasse Auto: __, Auto oldtimer; Auto* const this fcoldtimer; r2 Inteirt existiert hiermit jetzt rifi konstanter Zeiger this auf ddi Objekt m t dem Brrcl ebner oldtimer Einen yokhen Zeißc* k.inn$t du natürlich nicht selbst definieren’ unsigned short Auto::get_leistung() const { return leistung; } Du haM rin Objekt der Kla$$c Auto mit drm ßwcichnrr oldtimer Wenn du jetzt folgenden Elementfunktionsaufruf machst oldtimer.get_leistung() ...wird ja folgende Elementfunktion aufgerufen: Der Zugriff auf das private Element leistung erfolgt intern mit dem this-Zeiger wie folgt: unsigned short Auto::get_leistung() const { return this->lelstung; } Somit entspriehl die Rückgabe von leistung innerhalb der Funktion this->leistung. In der Praxis spricht übrigens nichts dagegen, explizit den this Zeiger in einer Element funktion anzugeben. Das könnte bspw. nützlich sein, um itn dümmsten Fall zwischen gleichen Bezeichnern einer lokalen Variablen einer F.lementfunktion und den privaten Daten der Klasse zu unterscheiden. Gleichnamige Bezeichner zu verwenden, ist allerdings eher Bockmist, und den solltest du bleiben lassen. r-T-T-T-T-T-TT-T-T»" 7V.t;z In der echten Welt wird der this-Zeiger eher für andere Zwecke verwendet. So kann dieser bspw. ein Objekt als Ganzes (genauer die Anfangsadresse darauf) benutzen (*this), um aus einer Elementfunktion ein Objekt als Kopie oder Referenz zurückzugeben Das aber ist eine andere Geschichte, die ich dir zu gegebener Zeit auf jeden Fall erzählen werde. 298 UfrUel MC UN
Die Geschichte von Objekten Klassen, Elementfunktionen und Objekte ... Du hast bereits viel über die OOP erfahren. Leider ist es gerade für einen Einsteiger in C++ recht verwirrend, den Überblick zu behalten. Und dabei werden gerne die einen oder anderen Dinge durcheinandergebrachc. Zum Glück hast du ja dieses Buch hier gekauft. |E mfiche Aufgabe) Klassen und Objekte werden gerne durcheinandergebracht. Versuch mit einem Satz, den Unterschied zwischen Objekten und Klassen zu erklären. Perfekt! Und wenn du nicht mehr 100% Bescheid weißt, denk einfach daran: Klas$e»Braut im Playboy" und .Objekt»»Braut neben dir"! int main() "1 ein leerer Vektor de-r KUw Auto <iostrearo> <vcctor> "Aurci.h" vector<Auto> Stockcar; *1 linclude linclude linclude using namespace std; ISchwierigt Awfg.ibr| Erstelle ein weiteres Hauptprogramm, welches dich abfragt, wie viele Objekte der Anwender von der Klasse Auto anlegen will, und lies dann in einer Schleife diese Anzahl der neuen Auto- Objekte ein. Am Ende gibst du alle eingegebenen Objekte wieder in einer Schleife aus. Stopp! Bevor du dir daran jetzt die Zähne ausbeißt, verwende blue keine dynamische Speicherverwaltung mit new und delete. Die Sachen mit den Zeigern und der dynamischen Speicherverwalturig gehen meistens daneben. Ein Tipp: Verwende die Klasse vector! Okay, hier eine mögliche Lösung: 209
Auto temp; *2 unsigned int unsigned int "2 eih temporäres Objekt der Klam Auto, mit dein wir die privaten Daten in der Schleife einlesen und jeweils ans Ende rlr* Vektor* hangen c; val; cout « "Wie ein » c; '3 viele Autos willst du aruneldent *3 for(unslgned inc 1=0; 1 < c; 1++) M ( cout « "\nLeiscung : ein » val; tcmp,set_lcistung(val); cout « "Baujahr : "; ein » val; temp.set_baujahr(val); cout « "Höchstgeschwindigkeit : ein » val; temp.sec_hoechstgeschwindigkelt(val); Stockcar.push_back(temp); '5 > cout « "\nFolgende for(slze_t i«0; 1 < < cout Autos sind angemeldet:\n"; Stockcar.size(); !♦+> *6 "Leistung : " Stockcarfi].get_leistung() « endl; "Baujahr : ” Stockcarll].getbaujahr() « endl; "Höchstgesch.i " cout cout « Stockcar[i].get_hoechstgeschwindigkeit() « > return 0; (Schwierige Aulgabel Du hast zuvor auch den this-Zeiger kennengelernt, welcher in jeder nicht-statischen Elementfunktion als versteckter Zeiger auf das Objekt enthalten ist. wenn du das Objekt bspw. mit dem Punkteoperator und der Elementfunktion aufgerufen hast. Schreib ein kleines Beispiel mit einer neuen kleinen Klasse, welches demonstriert, dass die Adresse des Objektes und der Inhalt des this-Zeigers identisch sind *3 Mief kann der Anwender entscheiden, wie viele Objekte der Klasse Auto zum Vektor hinzugefügt werden dürfen. *4 tn der Schleife lesen wir die privaten O.ilen der Klam AutO in das temporäre Objekt temp ein . ‘5 ... und bangen es .in* Ende des dynamisch wachsenden Vektor*, welcher Objefctc der Kl.rtse VCCtOT Und zu guter Letzt geben wir wie gefordert, alk Daten der Objekte aus, welche Im Vektor gespeichert und Über die Eiementfunkt»on s 1Z 0 () aus der Klasse v vector erhältst du die Anzahl de* Elemente und gleichzeitig die Abbruchbedingung für die ScMeife. Wesentlich komfortabler kannst du es dir natürlich mit Ct + 11-like for machen, indem du die Elemente mit for( Auto : Stockcar ) durchläufst und dann eben über a.get^laistungt) usw auf dm einzeln en Elemente zugreifst’ "\n\n' 300 hCur
Viel| einfacher, als du denkst! Hier eine Musterlösung: dass ClassForOne < public: void Speicheradresse) { cout « "Adresse this } this « endl; ‘1 *1 Die Adresse dc$ thisZc*£Cf* entspricht int main() ClassForOne einObjekt; cout « "Adresse einObjekt einObjekt.Speicheradressen; return 0; fceinObjekt « endl; ’2 Der Beweis - das Programm bei der Ausführung: Dieter Baer $ ./Cla Adresse einObjekt Adresse this Dieter Baer $ | 0x7fffSfbff97f : 0x7fff51bff97f Per Beweis *2 .. der Adresse1 de* Objekte, welche* mH dewn Kon trat aufgerufen wird. (Bd4taun£/I6fufig| Ich kann dich einfach nur loben, wie toll du bisher mitgemacht hast. Ich weiß, das Thema ist nicht ganz einfach, aber mit ein wenig Selbstdisziplin machbar. Wie wäre es, wenn du dir mal wieder eine Runde WoW gönnst? Du bist ja jetzt schon länger nicht mehr dazu gekommen. c ] mtn 301
Was bisher noch fehlt, sind ein paar Heinzel- männchen. welche beim Anlegen eines Objektes eine Initialisierung aller privaten Daten übernehmen Sicherlich könntest du hierfür eine weitere Elementfunktion wie init (unsigned short, unsigned short, unsigned short) erstellen. aberO* hat da wirklich solche Heinzel- männchen eingestellt, und zwar Konstruktoren (auch leurz ctor genannt)! Mit solchen Heinzelmännchen kannst du sichcrstellen, dass beim Anlegen eines Objek- tes sofort mit gültigen Werten gearbeitet wird. Solche Konstruktoren sind den Element- funktionen sehr ähnlich. Nur unterscheiden sich diese durch folgende zwei Punkte: Der Name des Konstruktors ist derselbe wie der Name der Klasse, w Der Konstruktor hat keinen Rückgabewert (auch kein void am Anfang), *1 Oer Compiler tfcllt um drn Standard» kpmtruktor zur Verfügung, *2 Sokh ein Zugriff itt nkht gdrv unpfoblerrubixh. Ei i$t nicht garantiert, da^s h rr dai private Leistung vom SUndafdltonstruIrtor mit 0 initialisiert wurde. Zunächst wären da die Heinzelmännchen, die nur Dienst nach Vorschrift machen. Die Rede ist vom Standardkonstruktor. Diesen Konstruktor hast du übrigens bei dei- ner Klasse Auto auch schon verwendet. Wenn du nämlich überhaupt keinen einzigen Konstruktor in deiner Klasse definierst, stellt dir der Compiler einen Standardkonst- ruktor zur Verfügung. Großzügig, nicht? Aufgerufen wird dieser Standardkonstruktor, wenn du ein Objekt wie folgt definierst: Auto alteKarre; Auto* autopointer • new Auto? *1 *1 Beide Male werden di Standardkonstruktoren aufgenjfcn und machen Ihren Dienst nach VorKhntt Die impliziten, vom Compiler zur VerJügLing gestellten StandardkünsLrukloren garan- tieren dir allerdings nicht, dass die Daren automatisch mit 0 initialisiert werden, Zum Beispiel: Auto alteKarre; *1 cout « "Leistung : " « alteKarre,get_leistung() « endl; *2 [Zettel) Natürlich kannst (solltest) du hierbei auch die vereinheitlichte Initialisierung von 0*11 wie "Auto älteKarre{}" bzw "Auto* autopointer = new Auto{}" verwenden, womit man deutlicher erkennen kann, dass du einen Konstruktor ohne Argument aufrufst. Wenn es dein Compiler kann, empfehle ich dir, immer diese neue vereinheitlichte Initialisie- rungssyntax zu verwenden. Um sich also vor eventuell unmoti- vierten Heinzelmännchen zu schützen, solltest du dich nicht auf deren Gemüts- zustand verlassen und immer auch einen selbst geschriebenen Standard- konstruktor für dein Programm definieren. 302 hcun
lAchtungl Beachte bitte, sobald du mindestens einen selbst geschriebenen Konstruktor benutzt hast, stellt dir der Compiler keinen Standardkonstruktor mehr zur Verfügung! Allerdings sollst du den in der Regel ja ohnehin selbst schreiben! Willst du das Gestricke nicht Dritten überlassen (solltest du eigentlich nie), dann musst du den Standardkonstruktorselbst schreiben. Zunächst die Deklaration in Auto.h: |Hintrrgrundinfa| Der Standardkonstruktor wird immer dann verwendet // Auto.h dass Auto < public: '1 AutoO; *2 }; *2 Hier deklarieren wir de« Standard- konstruktor 1 Es ist w ganz be^arwlrrer Wichtigkeit <•). dass dH? Konstruktoren im public- und nscht kn private Bereich der Klassendefinition stehen Ajisömsien wlre ei nicht möglich, em Objekt def Klasse zu definieren, weil ja der Konstruktor gar nicht aufgerufen werden konnte’ wenn bei der Definition eines neuen Objektes keine weiteren Parameter angegeben wurden. Also: // Verwendet Standardkonstruktor Klasscnnamc Bezeichner; // oder C++ll-like mit vereinheitlichter // leerer Initalisierungsliste Klassenname BezeichnerO; Gewöhnlich wird auch hier wieder zwischen Deklaration In einer Headerdatei und Definition in einer Quelldatel unter- schieden. Jetzt noch die Definition des Standardkonstruktors in Auto.epp: // Auto.Cpp Auto::AutoO ( *1 *1 Der Zugriff muss hier natürlich auch über den Klassen« namen und den Zugriffsopcratcr :: erfolgen. '2 Hier initialisieren wir alle private Daten mit 0 Natürlich kannst du auch andere Standardwerte dafür «wenden, sofern diese 5«nn machen Wenn dein Compiler 0^11 kann, dann kannst du auf das Initialisieren leistung=O; *2 hier verzichten und Mj.11 dessen die . , , - , Daten dirrfet in der Klasse mit einem , d t * , <i., r U , Wert versehen’ Dazu gleich mehr . hüechSt8e.«hwindl6keit-O5 U -3 ßei dlwr d„ objeMes > IcercKarre von der Klasse Auto // main.cpp Auto leereKarre; “3 wird jetrt der eben erstellte Standard* konstruktcr verwendet1 Wenn dein Compiler C*f 11 kann, empfehle ich dirr hier gleich die vereinheitlichte Imtialisicrungssyntax zu verwenden 303
(Achtung] Wenn du ein -Array erstellst, dann darf dei- ne Klasse entweder gar keinen Konstruktor haben, oder du musst den Standardkonstruktor hin- schreiben! Wenn deine Klasse nur Konstruktoren mit Parame- tern (Thema kommt gleich) enthält, kann kein Objekt-Array wie folgt erzeugt werden: Es ist durchaus gängige Praxis, neben dem Standard- konstniktor viele wehere Konstruktoren (nach dem Prinzip der Funktionsüberladung) mit Parametern zu schreiben, um möglichst viele Versionen beim Erzeugen eines Objektes abzudecken. Das Prinzip ent- spricht dem bei den Elemcntfunktioncn. Die Deklaration eines Konstruktors mit Parametern; ff Auto.h dass Auto -2 ein weit. • • * 1 de« Standardkonstruktor Konstruktor mit public s Parametern Auto(); *1- J Auto(unsigned shorc, unsigned short, unsigned short); *2 Die DEFINITION eines Konstruktors mit Parametern: // Auto.epp Auto::Auto(unsigned short ltunsigned short btunsigned shorc h)‘1 { sct—leistung(1); setbaujahr(b); set_hoechstgeschwindigkeit(h); > // main.cpp Auto oldtimer(89» 19^i8, 166); “2 Auto hummerf 350, 2006, 206 ); II OHL-like ’1 (ta Definition des Konstruktors nut Parametern. Ich denke, n ist offensichtlich, >vas mit den drei Parametern hier geschieht...! lAblAje] So wie eben gezeigt, kannst du natürlich noch viele weiten mit unterschiedlichen Parametern zu deiner Klasse hinzufüj •2 Wenn du ein Objekt jetzt Sö erstellst, wi^d der Konstruktor mit den drei Parametern verwendet Konstrukteure n, 304 ur.t«l MCUN
In unserem Beispiel ist die Methode, die privaten Daten mit einer Zuweisung im Anweisungsblock des Konstruktors zu initialisieren, ganz gut geeignet. Sobald du aber in deiner Klasse ebenfalls Objekte als private Daten hast (bspw. String, vector oder andere bzw. eigene Klassen), dann solltest du eine Initialisierungsliste (oder auch Elementinitialisierer) dafür verwenden! Erweitern wir bspw. den privaten Bereich der Klasse Auto um ein string-Objekt: II Auto.h short lelscung; short bauJahr; short hoccbstgeschwindigkeit; *1 Mit der String fugen wir weitere p*rwate Daten zur KUssen- dcfinition von Auto hinzu. Nur handelt ei sich jetzt ebenfalls um eine Klasse wa* ja bedeutet, dass eben- falls ein Objekt daraus erzeugt wird. dass Auto < private: unsigned unsigned unsigned string marke; *4— public: Auto(); Auto( unsigned short unsigned short unsigned short string); *2 Auch den Konstruktor hiben wir um string für die Marki Auioi etwei’ (Ze'ltlJ Bei de> Deklaration des neuen Konstruktors ist weit und breit nichts von der Initialisierungs- liste (oder einem Element- initialisierer) zu sehen. Dies ist auch nicht nötig, weil diese erst bei der Definition des Konstruk- tors benötigt wird. Die Definition des Konstruktors mit Initialisierungsliste: // Auto.epp Auto::Auto( unsigned short 1, unsigned short b, unsigned short h. string m ) : marke(m) '1 < set_ie^stun8f D » *1 Hier «Mit das private Objekt set baujahr(b); marke den Wert in direkt in der set_hoechstgeschwlndigkeic(h); Initialisierungsliste' > Ohne Initialisicmngslistc würde das private String-Objekt der Klasse Auto zunächst mit einem leeren String belegt. Wenn dann der Konstruktor seine Arbeit ver- richtet. wind diesem string Objekt erst sein eigentlicher Wert übergeben. Also doppelte Arbeit im Speicher, die du mit der Initialisierungsliste vermeiden kannst, und ja, du kannst hiermit auch die gewöhnlichen 305
£trir^i» Basistypen initialisieren: Auto::Auto(unsigned short 1, unsigned short b» unsigned short, h» string eb) : leistung(l), baujahr(b), hoechstgcschwlndlgkelc(h), ®arkc(tn) { } €+♦11 hebt die Einschränkung der direkten Initialisierung von Klasscnelcmcntcn auf, was bisher ja nur mit statischen, konstanten Elementen integralen Typs möglich war. Du kannst also quasi mit C++11 deine Klassen- daten initialisieren: dass Auto < private: unsigned shorc unsigned short unsigned short ‘1 Neu in C++11! Die KUsseo- elemente werden direkt initialisiert. lelstung-O; ‘1 baujahr^O; '1 hoechstgeschwindigkcit-O; "1 std::string marke •'keine Marke"; *1 public: AutoO { }; *2 Auto leere$Auto{); '3 Klassennatne(); *S Du hantient hier ß.iranticfl nicht mehr mit nicht-initialisierten Sachen herum, *2 Damit ip.mt du dir eine Menge Tipp- Arbeit. Indern du z & auf dx» initialrslcaing der KU-sw/ideme-me beim SUndard-Koivst- ruktöf veuichten kannst. Auch hier gilt: Wird kein eigener Destruktor geschrieben, teilt dir der Compiler eine Standardversion davon in einem ublic-Bereich zur Verfügung. Einen Destruktor DEKLARIEREN: Bei dei Definition des Oestruktors musst du natürlich wieder gegebenen falls den Klassennamen ♦ Zugriffsoperator mit angeben, wenn du diesen nicht innerhalb der Klassrndefinition definierst. Wird ein Objekt nicht mehr benötigt, wird das Gegenstück vom Konstruktor aktiv: der Destruktor (kurz auch dtor genannt). Der Destruktor ist für das Sau- bermachen zuständig. Das können Dinge sein wie Speicher freigeben. Sperren aufheben, Dateien oder sonstige Ressour- cen freigeben usw.! Wie der Konstruktor hat der Destruktor denselben Namen wie die Klasse. Um den Destruktor aller- dings von dem Konstruktor unterschei- den zu können, wird das Tilde-Zeichen (~) davorgesetzt, im Gegensatz zum Konstruktor gibt es allerdings nur einen Destruktor, und dieser hat auch niemals einen Parameter und gibt auch nichts zurück. Einen Destruktor DEFINIEREN: Klassenname: t-KlassennameO { H Anweisungen für den Destruktor > 306 C«okt«l HCUN
Friihjahrsputz An sich macht cs bisher noch keinen Sinn, einen speziellen Desirukior in unserem Beispiel zu schreiben, weil wir ja keine geson- derten Daten freigeben müssen. Trotzdem kann das an dieser Stelle doch ganz lehrreich sein, einen solchen Desiruktor mal selbst zu konstruieren, um zu sehen, wann dieser eigentlich mit dem Saubermachen anfangt. (Eiwfjchr Aufgabe-) f J Fuge einen Destruktor zu unserer Klasse Auto hinzu, der nichts anderes macht, als die Texlfolge „Auto in die Schrottpresse" mit der Marke (marke) auf dem Bildschirm auszugeben. Die Deklaration des Destruktors in Auto.h: dass Auto { private; public: AutoO; Auto(unsigned short, unsigned short, unsigned short, string); —AutoO» *1 den Owtniktor a a ' dekl.inerrn Jetzt die Definition des Destruktors in Auto.epp: II Auto.epp Auto::-AutoO < cout « marke « " in die SchrottpresseVn”; > 307
Prima! Anhand der folgenden main-Funktion kannst du jetzt sehr schon erkennen, dass der Destruktor eines Objektes immer dann aufgerufen wird, wenn der Gültigkeitsbereich verlassen wird. Im Grunde exakt genauso wie bei den Basls- datcnlypen. Hier eine main-Funktion zum Ausprobieren: linclude <Lostreana> linclude "Auto.h" using namespace std; J •1 Das Objekt Welt Auto ist global und der Destruktor wird d.ibe< mit dem Ende der main Funktion f ausgcfyhrt. Auto WcltAuto{99» 1998, 172, ‘'Welt-Auto*} j int main() < Auto *TestAuto new Auto{75» 2003» 155, "'Te Auto Volk$Auto<65» 2010» 164» "Volks-Auto1"}; delete static < Auto > return > *3 Das lokale Objekt VolksAuto wird am Ende des Anweisungsblocks (hier beim Ende von main ( )) 'wwn Dntruktoz brsr tigt *2 Der DüSlruktor et dynamischen Objektes Test Auto wird in Ort und Stelle .lufgrrufcfi, wenn das Objett mit delete zerstört vrrd TestAuto; *2 Auto SAuto{55, 2001» 145» "Static-Auto"}; ChinaAuco{40, 2002» 154» '•China-Auto”}; 0; irfotiil Hier wurde die vereinheit- lichte Initialisierungslistc mit {} verwendet. Falls du einen alten Compiler hast, musst du runde Klammern 0 statt- dessen verwenden. ’S 0a% lokale Objekt ist nur innerhalb des Anweisungs- blocks gültig, weshalb der Destruktor hier bereits außerhalb des Mlni-Anweisurtgsblocks wieder aufeerufen wnd, um das Objekt zu zerstören. *4 D.w Objekt SAuto Ist static. weshalb der Destruktor auch htcr erst nut dem Ende des Programms aufgetufen wird. A _________S<h»C>ltpe»Y>» -> DevlrukW Dieter 8-i—r $ ./scrapyanl lest-Äulo in dir SrhrqUprcsie China-Auto m dte Schrottprevsc voUi’Autp in die Schrottpressc Static-Auto in die Schrott presse He^-Autc In dir Sch rottpresse Dieter tjaer $ | Das Programm bei Ausführung: lEunfacht Aulgabel Füge der Klasse Auto einen weiteren Konstruktor hinzu, welcher ein Objekt erzeugt, wenn der Anwender nur die Marke bei der Instanziierung angibt. Die anderen privaten Daten sollst du dann mit 0 initialisieren! 308 hCün
Genau, das meine ich! Und du hast es auch schon gut betont: .das string Objekt"! Du weißt, was das beim Initialisieren bedeutet!!! Enttäusch mich nicht! Okay, hier die Deklaration in Auto.h: fl Auto.h dass Auto { private: unsigned short leistung; unsigned short baujahr; unsigned short hoechstgeschwindigkeit; *1 Für <Us Iniliahtieren von Objekten wird jj. die Inlttalisterungs- liste empfohlen, wol diese wesentlich effizienter ist« > Die Definition des Konstruktors in Auto.epp II Auto.epp Auto::Auto(string m) : marke(m) { *1 lci.stung-0; baujahr»0» *2 hoechstgeschwindigkcit«0; M > string marke; public: Auto()» AutO(string); '1 di« Deklaration des neuen Konitiuktors *2 Dir rrsMKtien privaten Daten initial vieren wir ganz trocken md 0. Wenn du C+411-kkc die direkte Initialisierung von Klasscnelenscnten durchgcfahrt hast, kann« du dir diese Zeden selbstverständlich sparen, fl main.epp Auto AutoOhneDatenf "Rennsemmel11); J ’3 Dank dei neiden Konstruktors Auto(string) k.vinj- tzt uch so ein Objekt mil gültigen Werten erzeugt werden Saugut gelöst und vor allem prima aufge- passt mit der Initialisierungsliste und dem String Objekt. Zwar wäre es mit einer Zuweisung auch gegangen, aber das Thema mit der doppelten Arbeit haben wir ja schon besprochen! C UJJtn 309
Komm schon, so schlimm war es auch wieder nicht. Pass auf ich fasse das nochmal ganz einfach zum Millesen zusammen. Olcay, der Abschnitt war jetzt schon extrem dicht gepackt. Zum Glück haben wir daher das Wohnzimmer, wo wir alles nochmal zusammenfassen kön nen/müssen. (K)eln Kartenhaus Und Moses sprach, hier sind meine zehn Gebote zu den Konstruktoren und dem Destruktor: 1. zum erzeugen eines Objektes brauchst du EINEN KONSTRUKTOR (KURZ; CTOR). UM DAS OßJl.K WIEDER ZU ZERSTÖREN WIRD EIN DESTRUKTOR * (KURZ DIOR) VERWENDE I. ES KANN MEHRERE KONS I RU KJ OREN GEBEN. ABER NUR EINEN DtSTRUKTORl 2. DIE KONSTRUKTOREN UND DER DESTRUKTOR HABEN DENSELBEN NAMEN WIE DIE KLASSE UND KEINEN IWCKGABI Wl RT. ZUR 1. N 11 ILSCH1 IDUNG WIRD VOR DEN DtSTRUKIOR DAS TlLDE-ZEICI IEN (~) GESETZT. 3. WENN DU KEINEN KONSTRUKTOR ODER DESTRUK- TOR SCHREIBST. S FE LL1 DIR DER COMPILER JEWEILS UNI Standardversion im PUBLIC Bereich zur Verfügung. 4. SOHAID DU MINDESTENS EINEN KONSTRUKTOR SELBST SCHREIBST. STELLT DIR DER COMPILER KEINEN STANDARD KONSTRUKTOR MEHR ZUR VERFÜGUNG. DANN MUSST DU DAS SELBST IN DIE HäNI) NEHMEN. 310
F 1 .H ’ 6. 8. DU MU>M KONSTRUKTOREN UND DESTRUMOREN IM FUKLic Bereich schrei rem. weh. du sonst keine Objekt e DAVON ERZEUGEN BZW. WIEDER ZERSTÖREN KANNST! Gl NI RI 11 WIRD SOWIESO EMPFOHLEN. EIGENE KONSTRUK- TOREN ZU SCHREIBEN UND SICH NICHT DARAUF ZU VF RI AS- SEN. was Dir Heinzelmännchen mit den Daten machen. Neben dem Standardkonsirukior ohne Parameter KANNST DU AUCH KONSTRUKTOREN MIT (MEHREREN) Parametern erstehen, um möglichst viele ObiektEr- zeugungsvarianten ABZUDECKEN. DER DESTRUKTOR HINGEGEN DARF KEINE PARAMETER ENTHALTEN. OB|EKIE. DIE ALS PRIVATE DAIEN SELBST OBJEKTE ENI HAI I1N. SOLLTEN IMMER MIT DER INITIALISIERUNGSLISTE INITIALI- SIER! WERDEN. MINDESTENS DAS EINE OBJEKT , Beim initialisieren mit der Initialisierungsliste soll- test DU AUSSERDEM DU IlF.II IFNFOIGI I INHAJTI N. WI ICHI du bei der Definition der KiaSSe verwende, i hasi. Ganz besonders, wenn du bspw Zeiger als Daten hast, deren Grösse du noch nicht kennst. Das abbauln von Objekten mit dem destruktor ist abhängig vom Gültigkeitsbereich, innerhalb dessen DAS OBJEKT DLEINI1RT WURDE.. JO. OBJEKTE MIT KONSTRUKTOREN UND DESTRUKTOR SOLLTEN AUF KEINEN FALI DATEN EINER UNION ENTHALTEN. Das WÜRDJ BEI DEN UNIONS BEREITS KURZ ANGESPROCHEN! I ? '< Vh Nimm dir jetzt unbedingt Zeit, das hier Beschriebene zu verstehen, yonst verlierst du den Faden. Und wenn alles sitzt, dann gönn dir eine Pause und mach dir einen schönen Abend mit deiner Freundin. Führ sie mal wieder zu einem schönen Essen aus! 311
Jetzt gibt es noch ein paar Dinge, die wir noch genauer unter die Lupe nehmen müssen. Nein, eine Lupe ist hierfür nicht genau genug, hierfür brauchen wir schon ein Mikroskop. Ein weiterer Konstruktor, der auf Kosten des Hauses geht. ist der Kopierkonstruktor. Ohne dich um Weiteres kümmern zu müssen, ist dank des großzügig vom Compiler zur Verfügung Zuwlcisung gestellten Kopierkonstruktors Folgendes möglich: U\Vr^eix'Cbi'-''1 Kopie TestKar«""’ XestKarre Ml Auto Auto Auto {Acht TestKa r re; Kopie_TestKarre = TestKarre; *1 Kopie_Kopie_TestKarre(Kopic_TestKarre); ‘2 FaMvbei aten deiner Klasse dynamische Elemente (Zeiger) vorhanden sind, wird dies mit Sicherheit zu Problemen mit dem Standardkopierkonstruktor führen. In dem Fall musst du einen eigenen Kopierkonstruktor schreiben. Wenn also der Standardkopierkonstruktor vom Compiler nicht zum gewünschten Verhalten führt kannst du mit folgender Syntax einen solchen Kopierkonstruktor selbst schreiben: Klassennamel const Klassenname & ); f Auch hiermit wird ein neues Objekt Kop ie_Kopie_Tc s t Kar re wieder mit dem gleichen ”— ln-i.i t erzeugt. 312 MCUH
Bezogen auf unsere Autoklasse sieht die Deklaration eines Kopierkonstruktors wie folgt aus: fl Auto.h • • publict Auto( const Auto Et ); Die Definition des Kopierkonstruktors hingegen kannst du wie folgt schreiben: // Auto4 epp *2 Damit du siegst, dass- ei sich hier um eine Kop<* handelt, habe ich ex per im en- teil die Textfoljjr , - Kopie’ angeh^figt’ AutosiAutOl const Auto ia ) { leistung = a.leistung; *1 baujahr - a.baujahr; "1 hoechstgeschwindlgkeit-a.hoeehstgeschwindigkeit; *1 marke = a.n&arke ♦ - Kopie"; *2 } *1 Damit auch das neue Objekt alle Elemente bekommt, musst du die privaten Daten Element für Element kopieren und dem neuen Objekt zuweisen. If main.cpp Auto Volk$Car(79, 19B9, 164, "FauWeh"); II bzw. C++ll-llke: Auto VolksCar{79. 1989 Auto copyFauWch • VolksCar; *3 cout « copyFauWeh-get_marke() « endl; *4 *3 Hw wird unser rige*ier Kopier- konstruktor aktiv. 164, "FauWeh"}; 4 Die AUrke (F.iuWeh Kopie). hat jettt der» ZusaU. - Kopie- JZtllcI] Noch besser ist es, wenn du auch beim Kopierkonstruktor (hier 1) gleich die Elemente mit der Initialisierungsliste befällst: Auto::Auto( const Auto ia ) : leistung(a.leistung)> baujahr(a.baujahr)t hoechatgeschwlndigkeltCftxhoechscgeschwindigkeic) { marke = a,marke ♦ " - Kopie”; 313
manuelle Erstellung des Kopierkonst- Aber wenn du bspw. dynamische Daten len Kopierkonstruktor selbst schreiben nai cm Konstruktor nur einen Parameter (wird auch Konvertier rungskonstruktor genannt), kannst du auch ein Objekt per Zuweisung erzeugen. Zum Beispiel kannst du dank des Konst- *1 Diese Kon- vertierung ?on const char*wüide auch mit einem Könitruk- tor Auto(string) funktionieren ruktors Auto(const char*) (den du zuvor hoffentlich von Auto(acring) geändert hast) ein Objekt mit folgenden Zeilen erzeugen: Auto TestKarre 01("Raclng Car”); *1 // C++11: Auto TestKarre_01{"Racing Car"}; *1 char car() trägster"; Auto TestKarre_02 = car; "2 Auto TestKarrc_03 - "Forme13000”; *2 “2 Hier wird der Konstrulctöi Auto(const char*) .iLsgelubrt und fuhrt implizit dir Konvertierung von const char* n«h string (m.r marke(tn)) ode- Auto: : SCt_ marke (st ring)) durch Ein Konstruktor wie Auto (st ring) wiirde hier nkht mehr funktionieren und semen Dienst verweigern. Nicht immer ist allerdings eine solche implizite und versteckte Konvertierung des Konstruktors per Zuweisung erwünscht, wie du es hier inii const char* nach String gesehen hast. Das kann eine böse Überraschung mit sich bringen, wenn eine implizite Konvertierung durchgelührt wird, wo keine erwar- tet wurde. Willst du keine implizite Konvertierung bei Konstruktoren mit einem Parameter» musst du dies explizit anfordern: /! Auto.h • 44 public: cxplicit Auto(const char *); *1 •1 H»er sciircxben wird bei der Deklaration von Auto(const char*) das Schlüsselwort Cxplicit vor den Konvertierungs- konstrukto*. /I main.cpp «2 Diev-explizite Auto TO$tKarrc_01 ( "Racing Car"); *2 Zuweisung funktioniert // C++1H Auto TestKarreOl {”Racing Car"}; *2 natürlich nach wie vor. char car[) = "Dragster"; Auto TestKarre_02 • car; -3 Auto TestKarre 03 « "Forme13000”; ’3 *3 Hier vchLi^t d.it Schluwlwort explicit gnadenlos zu O.u Programm IjssI sich nicht mehr übersetzen. Die implizite Konvertierung funktioniert jetzt nicht mehr1 Hier braucht cs Jetzt ebenlalls eine caphzite Konvertierung wie Auto TestKarrC—02{car}; und Auto TestKarre_03{"Formcl3000n}; 314 MCUN
Praxis Dr. Schrödinger Damit du mir nicht noch nachts von der Klasse Auto Albträume bekommst, sollten wir da nicht noch mehr rummurksen, sondern dir mal zu Abwechslung eine andere Klasse geben, an der du rumschrauben kannst. dass Lines ( private; char dash; unsigned int len; public: LinesO : dashf’-1), len(10) { Linesfchar d, unsigned int 1) : dash(d), lend) O Lines(unsigned int 1) : dash('-'), len(l) { void printDash() { for(size_t i"0; l<len; 1++) cout « dash; } cout « endl; Lines einfacherStrichCI’t 20); einfacherStrich.printDash() Lines nocheinStrich 30; nocheinStrich.printDash(); Lines einfacherStrich_Kopie * einfacherStrich; cinf acherStr ichKopie.. printDashO ; / Das Listing ist einfach gehalten und macht letztendlich nichts anderes, als das Zeichen dash insgesamt len mal mit der E lernen tfunkticm printDashf ) auf dem Bild- schirm auszugeben. 316
Das Programm bei der Ausführung: • n a c h e Impler-'pntiere in diesem Beispiel zur Übung einen Kopieptonstruktor, der die Anzahl fer einzelntjzeichen in len beim Kcqweren verdoppelt Außerdem wollen wir hier, dass der implizite Konvertierungskonslruktor Lines nocheinStrich = 30; so nicht aus- geführt wird und stattdessen explizit (Lines nocheinStrlch< 30)) erzeugt werden muss! Hier eine Musterlösung der Klasse Lines: wie gefordert. die Ver- doppelung der einjrlnen Zeichen* welche sxh beim nächsten printDashf)-Aufruf mrt dem kopierten Objekt wiederfinden, (Belohnung] Das hast du sauber hingebracht! Meinen Respekt! Zur Belohnung solltest du dir einen leckeren Donut oder Muff in gönnen, um dann mit frischer Energie weitermachen zu können. dass Lines ( privates char dash; unsigned int len; publics Lines() : dasht'-1), len(10) { } Linesfchar d, unsigned int 1) : dash(d), len(l) {} explicit Lines(unsigned int 1) : dash('-’>t lcn(l) O “t C— Llnes(const Lines &L) { len = L.len * 2; dash • Lidash; *1 Durch Voranstclten des Schlüsselwortes expllcit i$l der Anwender der Klasse jetzt gezwungen das Objekt explizit mR Lines linc(123) zu erzeugen Oie implizite Vc-rsior» mit dem Zuweisungsoperator _______ funktioniert jetzt nicht mehr. *2 Hier schreiben wir unseren eigenen Kopier- konstruMor 316 c<0-.t«i hCum
Wohnung von Dr. Schrödinger An sich war dieser Abschnitt recht entspannend für dich. Du hast hier erfahren, was ein Kopierkonstruktor ist und wie du selbst einen schreiben kannst. Wenn du keinen solchen Kopierkonstruktor schreibst, stellt dir der Compiler einen zur Verfügung. der dein Objekt Bit für Bit kopiert. Wenn dein Objekt aber Zeiger als Daten enthält, willst du einen eigenen Kopierkonstruktor schreiben, weil du sonst den Zeiger (also die Adresse) mitkopierst und alle Objekte damit aut dieselbe Adresse verweisen würden. Damit hättest du keine wirkliche Kopie, sondern alle Objekte würden mit demselben Inhalt arbeiten (was zudem noch zu schwerwiegenden Fehlern führen kann/wlrd), Folgende Abbildung soll dir diesen Sachverhalt deutlicher machen: r ptwats- puWre: dövvit ehdr*); 1T Spietze«^ HariiCFuftbaW*, Wegen des Zeiget* gegenscand02 musst du einen eigenen Kopierkonstruktor «mplemutieren, weil es Arger g<bt. da Han* und Dieter denselben Football beanspruchen Die PiakIs dazu knegst du noch vorgesetzt. Ebenfalls erfahren hast du hier, dass ein Konstruktor mit nur einem Parameter ein Konvertierungskonstruktor ist, der implizit auch eine Konvertierung mit dem Zuweisungsoperator durchführt. Eine solche implizite Konvertierung kann mit dem Schlüsselwort expllcit abgcstelh werden. Class*» 317
The Big Three :rup: Die Regel der .Großen Drei' wurde ausnahmsweise nicht von mir erstellt. Obwohl... die könnte eigentlich auch von mir stammen. Nein, die Regel stammt vom C++-Imperator selbst. Bjarne Stroustrup! [Wotiefcn?Üb<-n| Was. du kennst Bjarne nicht? Er ist ein netter Typ. der ursprünglich aus Dänemark kommt und großen Anteil an der Entstehung von C++ hat! Stro Und zwar besagt diese Regel: Wenn du mindestens einen der Konstruktoren, also den Kopierkonstruktor, den Destruktor oder den Zuweisungsoperator selbst geschrieben hast, solltest du auch die anderen beiden hinzufiigen und dich nicht mit den automatisch erstellten Versionen des Compilers zufriedengeben. Wenn du nämlich mindestens einen davon schreibst, bedeutet das ja, dass dir die Standardversion nicht genügt. Ähnlich verhält es sich dann auch bei den anderen beiden Standardversionen. Spätestens wenn deine Klasse Zeiger als Daten enthält, sind eigene Versionen unumgänglich. Nochmal zum Nachsprechen: Hast du einen der folgenden „Big Three“ geschrieben: "" Kopierkonstruktor ** Destruktor IMötitH-fli’LlbtA] Das Überladen von Operatoren lernst du noch gezielt kennen Das Thema ist natürlich Gegenstand eines eigenen Kapitels. dann solltest du die anderen beiden ebenfalls selbst schreiben. I Vrlotau n g/L osuAg I Echt toll, wie du mitmachst. Jetzt hast du fast schon die Grund- lagen zur OOP durch. Da die Themen allerdings nicht unbedingt einfacher werden, gönn dir immer ein wenig Pause. Geh an die frische Luft oder mach Sport, damit du deinen Kopf frei hast. 318 HCUN
An dieser Stelle muss ich noch auf ein paar spezielle Daten eingehen, die du in den Klassen verwenden kannst. Da wäre nämlich noch die Möglichkeit, konstante, dynamische und statische Daten in einer Klasse zu verwenden. Alle drei Spezialitäten brauchen eine Sondcrbehandlung. Es ist also unverzichtbar, über alle drei Bescheid zu wissen! Enthält deine Klasse konstante Daten dann ... ... ist eine direkte Zuweisung mit dem =-Opcrator an die Konstante nicht erlaubt. Dies musst du dann mit der Initialtsierungsiiste machen. II Auto.h *1 DerW<rtbaujahr privates unsigned short leistung; const unsigned shorc baujahr; a1 unsigned short hoechstgeschwindigkeit; string marke; ändert Sich ja -eigentlich bei einem Auto nicht mehr, weshalb diese/ auch sinngemäß konstant sein kann. “2 Das nachträgliche implementieren e»ne» Konstante wirft allerdings so seine Probleme au* Selbst der imp/ementierle Standardkomtrukto? oder die Elementlunktion Auto: : sct_bauj ahr ( ) funktionieren jetzt nicht mehr publict AutoO; ’2 Um also für den Fall der Fälle konstante Daten mit einem Wen ?u initialisieren, musst du dies per Initialisirrungsliste mit einem Konstruktor durchführen: Auto::Auto(const unsigned short b) : leistung(O), baujahr(b), hoechstgeschwindlgkeit(O)♦ nyirke("Unbekannt”) { ) (Achtung Bedenke allerdings, da« du beim Standardkonstruktor dann ebenfalls die Initialisierungsliste verwenden müsstest, um in diesem Fall die Konstante mit einem Standardwert zu initialisieren. An dieser Stelle kannst du sehr schon erkennen, was für Probleme auftreten können, wenn du nachträglich etwas in deine Klasse implementierst, was du vorher bei der Planung übersehen hast! das. K UJJtn 319
sc? Natürlich, warum denn nicht! Allerdings muss dir schon klar sein, dass der Zugriff auf die privaten Daten dieser Klasse dann nur noch lesend möglich ist! Ein Beispiel: *1 ein konstantes Objekt const Auto SolidCar; *1 *2 Das kannst du schon noch cout « SolidCar.g«t_inarke() « endl; SolidCar♦set_leisturig( 100); *3 *3 ... aber beim schreibenden Zugriff macht das dein Compiler nkht mehr mit1 Mbhge) Ite ein Anwender deiner Klasse trotzdem versuchen, mit einem konstanten auf eine schreibende Elementfunktion zuzugreifen, dann kannst du zusätzlich Auto::set_leistung() noch eine konstante Version implementieren, die bei einem solchen Zugriffeine Fehlermeldung ausgibt. Zum Beispiel folgende Deklaration: . , _ . void setleistung() const; Enthält deine Klasse hingegen dynamische Daten ... ... die zur ljut'zeit mit new angelegt werden, empfehle ich dir umgehend, wenn möglich, diesen Aufwand durch fertige Klassen wie String oder vector zu umgehen. Wenn du aber vor einem Projekt sitzt, in dem die schon drin sind, dann musst du auch hier ein paar Dinge wissen: 1 Neben dem Zeiger für die dynamischen Daten brauchst du häufig auch noch einen weiteren Wen. mit dem du die Anzahl (oder Länge) der dynamischen Elemente sichern kannst - oder anders: Das Gummi sollte immer die passende Grüße haben. 2. Beim Stand*rdkonstruktor solltest du dafür sorgen, dass dir gleich zu Anfang min- destens Platz für ein Element zur Verfügung steht. Das musst du natürlich dyna- misch zur Laufzeit mit new anfordern - oder anders: Hab mindestens immer ein Gummi dabei. 3. Natürlich brauchst du zusätzlich noch eine Elementfunktion, die zur Laufzeit stets weiteren Speicher zum aktuellen Element hinzufügen kann - oder anders: Lege weitere Gummis an einen erreichbaren Platz. 4. Du musst einen eigenen Kopierkonstruktor schreiben, weil sonst nur die Adresse des dynamischen Elementes in der Klasse mit kopiert würde. Und somit hätten alle 320 MCUN
Objekte lediglich denselben Inhalt bzw. dieselbe Adresse. Gefährlich wird dies, sobald eines der Objekte vom Destruktor zerstört wird, weil damit eigentlich das dynamische Element nicht mehr gültig ist und andere Objekte nach wie vor darauf zugreifen können - oder anders: Niemals ungeschützten Verkehr! 5. Von ganz entscheidender Bedeutung ist das Zerstören des Objektes. Und zwar solltest du den reservierten Speicher beim Destruktor mit delete [ ] wieder freigeben, weil du sonst ein Speicherlcck (Memoiy Leak) hast - oder anders: Keine Gummis mit Beschädigungen verwenden. Enthält deine Klasse statische Elemente ... ... so würden diese Daten nicht nur für ein Objekt, sondern allen vorhandenen Objekten zur Verfügung stehen, weil die mit Static deklarierten Daten nur einmal Im Speicher vorhanden sind. Solche statischen Daten können nützlich sein, um bestimmte Informationen zu speichern, welche für alle Objekte von Interesse sind. In unserem Beispiel könnten wir hiermit praktisch die Anzahl der Objekte in der Klasse Zwischenspeichern oder einen Wert zum Austausch von Daten zwischen den Objekten. Ein Beispiel dass Auto < private: unsigned unsigned unsigned string marke; short short short leistung; baujahr; hoechstgeschwindigkelt; *1 Alle künftigen Objekte haben denselben Weil Zaehler $innvch>r^ei5c solltest du dann auch bc* allen Konstruktoren und belPi Destruktor diesen Wert um 1 erhöhen bzw. verringern static unsigned int zach1er 0; *1 r^rundinf^) Merk dir einfach: Statische Mitglieder einer Klasse gehören nicht zum Objekt, und du kannst sie auch nicht über den this-Pointer erreichen. > i niemand trennen Nein, keine Sorge, ich will hier keine* Parallelen zu unmusikalischen Fußballern ziehen. Trotzdem gibt es noch ein paar alte Freunde, die einen Weg kennen, mit dem du mit globalen Funktionen auf private Elemente einer Klasse zugreifen kannst. Ein Codestückchen hierzu: 321
1 globale Funktion printMarkc () mit einer Auto-Relerenz als Parameter void prlntMarke(const Auto in) { *1 cout « "Die Automarke lautet : " « a.marke « endl; *2 > int mainO { Auto VolksCar(79, 1989, 164, "FauWeh"); printMarke(VolksCar); *3 return 0; > 2 UnverscMmter Versuch! Zugriff .ml private Daten (hie> marke; der Klasse *3 Aufruf der globalen Funktion Ohne weitere Vorkehrungen würde der Compiler hier schimpfen wie ein Rohrspatz, weil du auf private Daten deiner Klasse von außen zugreifen willst. Es gibt jetzt in der Tat einen Kumpel, der diese Datenkapselung aufbricht, damit du diese globale Funktion regulär verwenden kannst. Du musst diese Funktion nur mit dem Schlüsselwort f riend zu deiner Freundesliste hinzufügen (ähnlich wie bei .Gesichtsbuch').. Dabei reicht es aus. wenn du diese Funktion in der Klassendefmilion mit dem Schlüsselwon f riend deklarierst: II Auto.h dass Auto < public: friend void printtfarke(const Autofc); .1 <?tz1 klappt es auch nut der globalen Funktion Guten Freunden edkiubl nun doch alles! tAchtun<| Dein Freund hat aber nur Asyl beantragt und ist trotzdem kein offizieller Staats- bürger (Elementfunktion) deiner Klasse. Daher steht für die f riend-Funktion auch kein thisZeiger zur Verfügung?!! lHintrr<rundinfof Auch Klassen lassen sich tl Allerdings kann ich dir davon nur abraten, weil später zum Streit kommt, wenn du zu viel Multikulti rein- schreibst. Klassen zum Freund zo erklären, gilt als schlechter und weicht das OOP.Prinzip'szu stark auf 322 c*o>t«i h[un
*Gong* Dis letzte Runde wird eingeläutet So. Jetzt hüten wir die finale Runde zu den Grundlagen der OOP ein. Streng dich an. weil du hier noch eine ziem- lich anspruchsvolle Aufgabe vor dir hast, um dich dann als Meister aller Klassen bezeichnen zu können. Okay, jetzt wollen wir zu unserem kleinen Problem mit der Klasse Spielzeug zurückkehren, bei der rin privates Attribut der Klasse ein Zeiger war und wo im Falle einer Kopie alle Objekte dieselbe Adresse beinhalten würden! Hierzu das vereinfachte Listing, wo alles gleich wieder inline definiert wurde: If myToys.cpp dass Spielzeug < private: string gcgcnstandOl; char *gegenstand02; "1 public: Splelzeug(string gl, const auch *1 Unser Pfoblcmkind in der Klasse Klar, em echter C*♦-Programmierer verzichtet auf $o etwas und verwendet stattdessen die KUsse string wie beim gcgcnstandO 1 darüber. Aber wir brauchen hier ja auch einoo Anlass zum Schrauben! char *g2) : gegen«tandOI(gl) { gegenstandO2 new char|strlen(g2) + 11; rim»n 323
strcpy(gegenstandO2, g2); > void printAll() { cout « gcgenstandOl « endl; cout « gegenscand02 « endl; > >; string ball "Ball"; char footbalH) • {"Football'*}; Spielzeug Hans(ball, football) ; *2 Hier legrn wir ein Objekt Hans mit rf<*m 6.1 »I und Football an, II C++ll-Hkc: Spielzeug Hansfball, football}; *2 Hans.printAll(); Spielzeug Dieter(Hans); H C+*ll-llke; Spielzeug DieterfHans}; *3 Dieter.printAll(); Der vom Compiler generierte Kopie/kooitruktor maeht eine flache Kopie von Hans und gibt diesen Inbilt .in Dieter Und so sieht das mit der flachen Kopie aus: Für die pädagogisch« F«ühkintf«i.wir>iijn( t*l hier der Standardkoptc*- komlnAlor nicht ge- eignet. Dm ruft nur da* Na'urrrcM a*d der* Plan (R«hi d« Stärkeren). $cM*mn« w« »d c* dann noch, wenn eine* der bei- den Objekte Freigejfeben ward, weil dann g*r kein «fftrieller FoolbaM mehr ‘rMhanden iit und dann da* andere Objekt auf einen nicht meta gültigen Adreitberekb verwert! Schwierige Aufgabe] Schlichte den Streit in diesem Beispiel, indem du den Kopierkonstruktor selbst schreibst und jeder seinen Football bekommt. Schreib außerdem auch einen passenden Destruktor, der das Spielzeug auch wieder aufräumt! 324 c*oit«i hcun
Jetzt komm schon, das Beispiel dient als Übung, und wir gehen jetzt mal den dornigen Weg und schlagen uns durch! Probier cs einfach aus! Es ist einfacher, als du denkst. Hier meine MUSterlÖSUHg // myToys.cpp dass Spielzeug { private: string gcgcnstandOl; char *gegenstand02; public: *1 der selbst getthrif-tw-nc Kopier- kgnslruktor ’2 Während sich der gegenstandOl einfach nur so zuweisen llsb? . .. SpielzeugCconst Spielzeug -Ls) < *1 gegenstandOl » s.gegenstandOl; *?’ gegenstand02 - new char(strlen(s.gegenstand02) ♦ strcpy(gegenstand021 s.gegenstand02); '3 müssen w>r beim gcgcnstand02 erst nii neuen Speicher anfofdern und dann umständlich mH einet alten C-funktion strepy ( ) kopieren. Aber jetzt haben wir einem gegenstand02 m t tlner styvm Adressti Mit hast du eine tiefe Kopie erstellt! I); f -Spielzeuge) { *4 delete [J gegenstand02; *5 cout « ‘'Spielzeug weggerÜumt\nK; *4 Der Destruktor *5 . muss den von der Speicherte Ide angeforderten Speicher *uc gcgenstand02 unbedingt expert mit delete wieder heigeben, weil et sonst ein Spekhedeck ßibt' Einfacher könnte« du di» das Leben machefi, indem du den unlque_ptr für gegenstand02 vefwcncfctf! Dann musst du dich zumindest um die Freiß.ibe vom Speicher nicht mehr kümmern' Wenn du alle Daten exklusiv Stück für Stück von einem Objekt in das andere kopierst, spricht man von einer tiefen Kopie (deep copy). Wenn die Kopie eines Objektes hin- gegen die Daten mit einem anderen Objekt teilt, spricht man von einer flachen Kopie (shallow copy). Eine flache Kopie ist es also, was dir dein Compiler anzubieten hat. Eine tiefe Kopie musst du dir selber machen (Einfache Aulgabel Folgende Codezeile lässt sich nicht ausführen. Warum? Spielzeug *toy per • new SpielzeugfS); Spielzeug() / Geht nicht ... Volltreffer! Völlig richtig! Das hast du dir sehr gut gemerkt! Respekt! 325
ff inyToyS,cpp dass Spielzeug { public: (Schwierige Ai/fg-ibrl Schreib eine Elementfunktion, mit der du zwei Objekte miteinander austauschen kannst. Denk daran, dass du in der Elementfunktion mit *this als Ganzes auf das Objekt zugreifen kannst. Gut, ich helfe dir mit der Tauschfunktion: •1 d»e Elemenl- funktkxi Tauschcn() mit dem Tauschpartner als Adresse Dec Tauschpartner bekommt die >saressc aes durru-enoen Obiektei. *2 Die Adresse des Tauschpartners wird In einem neuen lemporlien Objekt zwischen- gespeichert" void Tauschen(Spielzeug *3) { M Spielzeug terap = *s; *2 *this = teop > >• "4 Das auhufende Objekt hingegen bekommt den Inhalt des Tauschpartners. Der Aufruf der Elementfunktion. Hans tauscht h < seine SpieHacbcn mH Dennis char football[] - {"Football"}; char basketball[) - {"Basketball”}; Spielzeug Hans("Fussball", football); // C++U-llke: Spielzeug Hans{"Fussball", football}; Spielzeug Dennis("ABC-Würfel", basketball); II C++ll-like: Spielzeug Dennis{"ABC-Würfel", basketball); Hans.Tauschen(kDennls); *5 |Ei«Ueh» Au.gibe] r Hm, denk mal nach! Was könntest du dagegen machen? Oder anders, was fehlt noch? rc Ich zwitschere nur: „Big Three“! 32t MtUH
Exakt! Damit das Beispiel hier läuft, müssen wir noch den Zuweisungsoperator überladen. Da du das Thema noch nicht kennst, kriegst du hier den noch fehlenden Baustein vor die Nase gesetzt, damit das Beispiel läuft: /I myToys.cpp dass Spielzeug { publlci ♦ 11; "2 1 Der Kopf der Operator Überladung. Zuruckgegcb™ wird ein Spielttug- objekV Spielzeug^ opcracor*(const Spielzeug &s) gegenstandOl s.gegenstandOL; gcgcnstandOZ - new char[$trlcn(s.gcgenstand02) «trcpy(gegenscand02, s.gegen$tand02); return *this; 1 Kode bearbeiten]! ‘3 Zurück* gegeben wird das.neu“ ifliiMiisierte Ob4rkt Als Ginjr\ i*this) •2 0*5 hier M wie sctwi beim Kople/konst- ruktof. Ko<f< b<-irbeiten) Bei opcrator=() sollte eigentlich auch noch ein if (thls 1 ) enthalten sein, damit du dich gegen eine Selbstzuwer- sung wie Spielzeug toy; toy=toy; schützen kannst. Wurdest du nämlich ein toy=toy; machen, würdest du ein Speicherleck produzieren!?} In dem Fall, dass thie==&s ist. kannst du auch gleich wieder *this zurückgeben. An dem Beispiel hast du gesehen, dass es recht ungemütlich werden kann, wenn du einen Zeiger bei den Daten deiner Klasse verwendest- Oftmals kannst du dir diese Qual ersparen, wie du selbst schon festgestellt hast, wenn du Klassen wie string oder vector verwendest IBrlotaungl ... 8, 9, 10, ’Cong*. aus! Prima, du bist mit den ersten Grundlagen der OOP ff durch. Zugegeben, dieser Abschnitt hier war recht hart, aber machbar. Bleib am Ball, und du darfst dich bald als C++- Guru bezeichnen. C 327
Kampfanalyse Ich weiß, die letzten Kunden waren han. Aber du hast dich tapfer geschlagen und bist hart im Nehmen. Bevor du jetzt das machst, wozu immer du auch Lust hast, wollen wir die letzten Runden noch ein wenig analysieren. Du kennst das doch vor und nach Sportveranstaltungen, wo ernannte Experten Ihren Senf dazu abgeben und auch noch Geld dafür kriegen. Das mache ich jetzt mit dir auch (und Geld kriege ich ja auch dafür). Im Grunde haben wir zuvor ganz spezielle Daten behandelt, welche deine Klassen beinhalten können. Fassen wir zusammen: » Konstante Klassen Daten mit dem Schlüssel- wort const kannst du heim Konstruktor nicht mit dem Zuweisungsoperator initialisieren, son- dern dabei musst du eine Initialisierungsliste verwenden. * Bei dynamischen Klassen Daten mit einem Zeiger wird es schon wesentlich komplexer und aufwendiger. Da der Compiler standardmäßig nur eine flache Kopie des Objektes macht, be- deutet das. dass du hier selbst einen Kopierkon- struktor schreiben musst, um eine tiefe Kopie zu erhalten. Bei einer flachen Kopie besteht das Problem, dass sich die Objekte die Adresse mit dem dynamischen Objekt teilen, was früher oder später zu einem großen Knall führen wird. Bei einer tiefen Kopie musst du erst auf einer neuen 323 Upttd MCUN
Adresse Speicher für das zu kopierende dyna- mische Objekt mit new anfordern, um es dann dorthin zu kopieren. Zusätzlich kommst du nicht drum herum, neben dem Koplerkonstruk- tor auch noch den Destruktor und den Zuweisungsoperator zu überladen (Stichwort: .Big Thrcc"), Verwendest du Stdtic bei den Daten deiner Klasse, steht dieser Wen einmalig für alle Objekte im Speicher zur Verfügung. Das stati- sche Mitglied hat praktisch keine Objekt- zugehörigkeit und ist so etwas wie eine globale Klassen variable, auf die alle Objekte zugreifen und die sic sich teilen können. Wenn du statt eines dynamischen Attributes in Form eines Zeigers in deiner Klasse die Klasse string oder vector verwenden kannst, mach es' Du sparst dir damit eine Menge an Arbeit und Nerven. Verwende also, wenn möglich, statt eines Ze-Strings wie char* die Klasse string und statt eines Ze-Arrays die Klasse vector1 Dafür wurden die Klassen u. a. auch gemacht! Willst/musst du trotzdem auf solche Zeigersachen in Klassen zurückgreifen, würde es sich zur Vermeidung von Problemen anbieten, gleich auf die neue C++11-Klassen unique_ptr oder shared_ptr bei dynamischen Klas- senelementen zurückzugreifen, anstatt new und delete zu verwenden. lEiaruhe Aufgabe] Mal sehen, ob du das mit den statischen Daten der Klasse ver- standen hast. Was verursacht im folgenden Codeausschnitt das Hinzufügen von gegenstand03? Was bedeutet das für Hans und Dennis? If myToys.de • « dass Spielzeug { private; string gegenstandOl; char *gegenstandO2; static string gegenstandOS; *1 •1 ein rtUfrchr? Mitglied in do ku« Spielzeug public: >5 string Spielzeug::gegenstand03 ” "ABC-Würfel”; *2 « « 4 char football[) - {'Football"}; char basketball[] {"Basketball1’); Spielzeug Hans("Fussball”» football); *3 Spielzeug Dennis("Fussball"» basketball); ’3 *2 Das fct nötig, weil das statixhc Klawnrlemrnt bereits eirw*n Speiche* besitz, obwohl n<xh kein Otyckl davon existiert Deshalb nww die Variable auch außerhalb der Klaise definiert und gegebenenfalls inltUli- siert werden’ *3 H«er werden die Objekte Hans und Dennis erzeugt 329
Hier die Lösung: Durck den itatache* Gr rv*uss«* sich allr xndrrvn dfn G+fmttnd (gCgCnStßndO 3) leiltn, Awch der Gegenstand dvrch einen anderen Inhalt wird, haben alle anderem Objekte einen An- spruch darauf. Ean Gegenstand lur alle Leute eben! Eine Aufgabe noch, dann bist du fertig hier. JEinfache Aufgabe] Die globale Funktion printGegenstandO1(} gibt den Inhalt von gegenstandOl eines Objektes, welches du als Parameter mitgibst, auf dem Bildschirm aus. Allerdings verweigert der Compiler die Übersetzung, weil wir ja von außen auf ein privates Mitglied der Klasse zugreifen wollen. Was können wir jetzt tun, damit die Funktion trotzdem ausgefuhrt wird? // royToys.de void printGegenstandOl(const Spielzeug &s) { *1 cout « "Spielzeug 01 : " « s.gegenstandOl « endl; } *1 Die globale Funktion bist Wh . « • , ,,, nicht aujführcn, char footballj] {"Football1'}; Spielzeug Hans{"Fussball", football); printGegenstandO I {Hans); *2 -jde» Furiktoni- Aufruf /c< Lösung in der Klassendefinition: ItrloKnun^/Lökumgl dass Spielzeug { public: *1 AK Freund .inierer Klibic itt jetzt ein Zutrift auf die privaten Mitglieder der Kl.nse m«t der globalen Funktion möglich’ friend void printGegenstandO 1 (const Spielzeug!»); ‘1 1 p So, jetzt hast du einen riesigen Löwenanteil der OOP rund um die Klassen kennengelernt. Ich empfehle dir daher unbedingt, eine Pause einzulegen. Vor allem kümmer dich um deine Freundin und dein Sozialleben. Es gibt im Leben noch weitaus mehr als nur, vor der Kiste zu hocken und Zeit in sozialen Netzwerken oder virtuellen Welten zu verbringen. 330 Mm*
p |10| überladen von । Operatoren Kino+WoW+ Programmieren s viel Spaß Schrödinger kann jetzt zwar Klassen erstellen, aber die wollen sich nicht so recht mit den Operatoren vertragen. Nicht mal einfachste Vergleiche funktionieren damit. Daher zeigen wir Schrödinger In diesem Kapitel, dass eine Operator-Überladung das Programm nicht physikalisch schwerer und unverständlicher macht, sondern, rlchtlddoslert, auch Spaß machen kann.
Du hast bereits niiterlebrn dürfen, wie der Zuweisungsoperator überladen wurde. Glücklicherweise ist es dir In C++ gestattet, dass du fast alle anderen Operatoren überladen kannst. Das Thema Überladen selbst wurde ja bereits bei den Funktionen behandelt (ich hoffe, du hast aufmerksam gelesen). Genauso ist es im Prinzip auch mit den Operatoren. Zum Zwecke der Operatorüberladung brauchst du natürlich noch ein Zauberwörtchen, welches du mit operator in C++ vorfmdest. Abgesehen von dem Zauberwort operator, kannst du diese spezielle Operatorverwendung wie eine gewöhnliche Elementfunktion schreiben. Bei den meisten Operatoren ist dies wirklich ziemlich einfach! Um einen Operator für ein binäres Pärchen aus dem Hut zu zaubern, muss mindestens Folgendes vorhanden sein: Äücfcgabewert f Klassennamen.*.*/operator & ( TYP ) Der Rückgabewert ist, ja, was wird er wohl sein, der Rückgabetyp des Operators. Das Zeichen $ hinter dem Zauberwort operator musst du durch das zu übcrladcndc Symbol für die Klasse* Klassennamen ersetzen. Mit Typ gibst du den zweiten, genauer rechten Operanden an. Elementfunktionsmäßig könntest du den überladenen Operator mit zwei Objekten einer Klasse so anwenden: Klassenname ObjektI, objekt2; Rückgabewert wert objektl.operator@(objekt2); *1 Eine Operatorüberladung kann übrigens auch als eine gewöhnliche globale Funktion geschrieben wer* den. Natürlich muss diese Funktion ebenfalls das Zauberwörtchen operator enthalten und ein Freund deiner Klasse (friend) werden. Ein zweistelliger Operator für eine Klasse hingegen sollte per friend- Funktion in der Klasse deklariert und definiert werden. 332 ichn
Zum Glück kannst du diese Überladung nach viel operatormäßiger benutzen und musst nicht diese Elemenifunktions- schrelbwelse verwenden. Das ist höchstens was für hochstapelnde Gccks! ’1 Der Ausdruck entspricht der Elementfanktioftsuhreibwcise von eben. @ fleusst du natürlich auch hier gegen den SO flßht’S Such Z entsprechenden Opento* tauschen. Klasaenname Objekt1, objekt2; Rückgab«w*rt wert - Objekt 1 @ objekt2; *1 Natürlich genießen die Operatorüberi ad Lingen nicht grenzenlose Freiheiten und können nicht der Schwerkraft strotzen. Folgende Benimmregeln der Operatorüberiadungen solltest du auf Jeden Fall kennen, bevor du diese aus dem Hut ziehst: *** Operatorüberladungen sind Klassensachen. Du kannst also nicht hergehen und die üblichen .irithmeiischcn Operatoren für die Basisdatentypen verbiegen. " Du kannst keine neuen Operatoren damit hervorzaubern. Es lassen sich also nur vorhandene Operatoren überladen. * Die Anzahl der Operanden kannst du nicht ändern. Ein binärer Operator bekommt nach wie vor zwei Operanden (Operandi + Operand2) und ein unsrer Operator bleibt auch bei einem Operanden toperand++). " Auch die Rangfolge der Operatoren verändert sich nicht. Auch nach der Oberladung gilt bspw. die Punkt-vor-Strich- Regclung. " Du kannst keine Standardargumente verwenden, und die Anzahl der Argumente, die der Operator eben hat, kannst du auch nicht ändern. Diese Operatoren können überladen werden: Operatoren j Was bin kh? Bei diesen Operatoren hingegen geht nix mit der Über- ladung: ttetcrl) Keine Regel, aber irgendwo sollte eine solche Operatorüberladung auch einen Sinn ergeben: Da du ja quasi frei bist, deine Operatoren zu überladen, sollte vielleicht auch der Anwender deiner Klasse erkennen, dass eine gewisse Logik vorhanden Ist. Auch sollte es logisch sein, dass du, wenn du bspw. den -Operator überlädst, das auch mit dem Gegenstück ! = machst. -—— + • * I X (tinir) ++ arithmetische *. +c .s *= / = 2= Operatoren && II 1 logische Operatoren == !=<<=> >= Vergleichsoperatoren & | • - » « St- |> » «« Bit-Operatoren *->->★<) |] & new delete new[] delete[] sonstige Operatoren | Operatoren | Was bin ich? • Zugriffsoperator Zugriffsoperator (Slement/eiger) ßcre^chjoperator ?: bedingte Auswertung sizeof Große von Objekten OB«rl#d«n ypn frpirUqrtn 333
Neben der gerade gezeigten Version« eine Operatorftberladung wie eine Elementfunktion zu sehreiben, kannst du dich auch weniger fett binden und nur eine Freundschaft eingehen. Wozu eine f riend-Funktion als Operatoriiberladung? Das werde ich dir jetzt schnell beschreiben. *2 Hier klappt dai nicht mehr, well als linker Operand von ♦ ein Objekt der KUssc (wartet wird Und hier steht jetzt ein konstanter Integer-Wert Für sotche Zwecke brauchst du einen Freimd Folgendes Beispiel einer ♦-Operator- Überladung sei gegeben: Klassenname Objekt; Int wert! Objekt * 12345; *1 int wert2 = 12345 + Objekt; "2, ‘1 Oie Definition hierzu wa'e int Klusscnnamc: :operator*(intval){ } Und so kannst du eine freundliche Operatorüberladung machen: fiückgabcvcrc operator @ ( Lopcrand, ROperand ) Der Rückgabewert ist natürlich das, was er immer ist. Das Zeichen @ hinter dem Zauberwort Operator musst du durch das entsprechende Operatorsymbol ersetzen. Der erste Parameter Loperand ist der linke und der zweite Parameter ROperand der rechte Operand, platziert jeweils an der entsprechenden Seite des Operators ^Hantergrundinfol Nicht vergessen: In der Klasse musst du diese globale Operatorüberladungsfunktion mit dem Schlüsselwort f riend zu deinem Freund machen! Aufrufen könntest du diesen Freund jetzt auf zwei Arten: operator@(vall, Objekt); vall @ Objekt; *1 Zugegeben, der Teil war sehr theoretisch. Aber im nächsten Teil gibt es dafür Praxis satt. Als kleine Überraschung darfst du dann dein eigenes kleines Rollenspiel basteln. 334 ZCMN
Die Pärchen verwenden die Operatoren Es war einmal ein Bösewicht, genannt Schwarzem Magilk. Dieser Bösewicht hat die Welt zu einem dunklen Ort werden lassen, an dem nur noch Angst, Terror und Armut herrschten und C++ verboten war. Es durfte nur in C programmiert werden. Nach vielen Jahren der Tyrannei, vielen Buffer- Overflows und Memory Leaks machte sich ein tapferer Ritter auf dem Weg, die Welt von diesem Elend zu befreien, um endlich wieder C++-Programme schreiben zu können. Der Edle Ritter nahm sämtliche Gefahren auf sich, um sich dem Schwarzin Magh« entgegenzustellen. 1 Ein Held wird Die Well wird vom .Schwarzen Magier* bedroh? Der af dk RHlff* ond die ,Brave He«* >ind die einzigen, dir %»<b •hm entgegeostellen. Ein Kampf auf Leben und Tod entbrannte Charactcr einHcldt"Edler Ritter", Characver einßoesewicht("Schwarzer einHeld.indenKaapf(einBoesewicht); Aber, seht selbst RiHr r” hat rw-w JO If bfmpenkt? <HP)4 kann aber mit einem Schl ig nur 5 Angriffspunkte (MP) beim Cf p»cr <nielfn und vo«ide»»f*i Lfbmjpunkun Ab-iithe« .Schwafler Migier hin^ege^ ist mit 18 Lcbf"ipvnktrn tMPO zwar nicht ganz se vbfHebeniWiig w»e unser Ritter, kann aber mit einem Angriff gleich iS TreHrrpunkle (AAPJ beim Cegner landen Und beim Schlagabtausch ist die Lebemeneegie von .Edler Ritter* damit bereits n*ch dem rweilen Schlag weg. 3 Der Kampf entbrennt 2 Ein Böstwkhl tyrannisiert die Welt. 20, 5) Magier", 18, 15); *2 •3
Es hilft also alles nichts, auch die Brave Hexe konnte alleine nicht gegen den Schwarzen Magier bestehen. Damit du das ganze Dilemma nach vollziehen kannst, brauchst du natürlich den Quellcode zu unserem ganz persönlichen Rollenspiel. Hier die Headerdatei von myWoW*h: II myWoW,h •1 Dir Daten unserer Hctden Der Name der KimpfedtUsse, die Lebei CHF) und die Angriffspunkte WP) - ei| natürlich Zauber punkt q-dber wir venl sic hier als Anrrif^ounklc, geil: Also, keine linclude «lostream* linclude <string> using namespace std; lifndef _MYWOW_H_ Idefine MYWOW H ’3 Ein pxv nützliche Elfi men Funktionen tu - u’iser Spiel. Audi hier nur duf das NdL^sU* blchrJinkt Du konntest ja r^h ZuflriffselcmentfunUigflrn ‘ dass Character ( private: string klasse; *1 Xlnt hp; *1 S»lnt mp; *1 publics Charactcrfstring, ints int); ‘2 void heilent); *3 void indenKampf(Charactcri ); *3 *2 Der Konstruktor für einen neuen ^/^(eldcn. Ist natürlich ausbauflhig^*1 abe« für unsere bescheidenen Absichten reicht das vorerst mal aus. Wenn du Lust hast, kannst du ja ein bauen für die Daten schreiben. void getScatusO const; *3 lendif Jetzt noch die Quelldatei von mytfoW.cpp: // myWoW.cpp linclude <iostream> linclude «string* using namespace std; __ linclude "myWoW.h" *1 der Konstruktor Character::Character(string s. int h» int m) : *1 klasse(s), hp(h), mp(m) { — cout « "Ein neuer Held ist dabei: gctStatusO ; cout « endl; *2 eine einfache Funktion, um d»c leberupunkte void Character::heilen() { '2 2u heilen 338 zchn
hp+“10; void Charactcr;: indcnKanipf (Charactcr &cl) { *3 vhlle(l) { this->getStatus(); cout « ” greift cl -hp-»this->tnp; if(cl.hp <- 0) { cout « cl.klasse « * hat verloren!In"$ break; } cl.getStatus(); cout « ” greift anl\n"; thls->hp-«cl .cnp; if (this->hp <-0 ) { cout « this->klasse « ” hat verloren\n"; break; } *3 Der Kampf in einer Endlowhleife. tn$ onem der beiden HcWn d*c Puste (HP) ausgeiit Der Schlagabtausch findet abwechselnd statt. Der Aufruhr <*thiS) der Elernenlkmktiön beginnt mit dem ersten Schl.iß gegen den Gegner nut Pj.r.inwtcf C *A‘ void Character: :gecScatusO { *4 - ~ cout « klasse « " (HP:1' « hp « "/HP: *2 Mit vereinten Kräften geht es jetzt gegen & hwjr/cr Marter in den Kampf. *1 Die beiden Helden ÄUIec und flfdvr Hexe bilden eine Allianz, um d;c HB und MB zusammenzuaddheren und zu bündeln. *4 eine Elcmentfunktion, um den nktudlrn Zmtand deines Helden ab?<dragen « mp « ") " ; Mit vereinten Kräften Und so soll dies gelingen: Es hilft alles nichts. Um den Schwarzen Magier zu besiegen, haben sich der Edie Ritter und die Brave Hexf. entschlossen, ihre Kräfte zu einer Allianz zu bündeln und sich so dem Bösen zu stellen. einHeldf"Edler Ritter", 20» 5); gutcHcxcf"Brave Hex«"» 10» 11); Allianz einHeld * guteHexe; *1 einBoesewicht("Schwarzer Charactcr Charactcr haraccer Character Allianz. IndenKampf (einBoc.sewlcht); Magier”, 18. 15); *2 oterl<don von ftpiritortn 337
Together we’re Strong: Erst eine Allianz kann der* B&sewicht «uthatt«*. "Jub«lg«schfri, Fatifur“ Damit eine Zeile wie Character Allianz einHeld + guteHexe; auch tatsächlich funktioniert, musst du hier natürlich den +-Operator entsprechend überladen. Die Anforderung an diese Opcratorübcrladung ist die, dass ein neues Objekt der Klasse Character zurückgcgeben wird. Als Parameter erhält diese Oberladung außerdem ein Objekt der Klasse Character. Die Funktion der Überladung ist die, dass die Daten hp und mp beider Objekte zusammenaddiert und als neues Objekt zuriiekgegeben wenden. Die Deklaration in myWow.h: II myWoW.h 4 4 4 dass Character { *1 Die geforderte Deklaration des ♦-Operators. • • publics Character operacor*(Character^); ‘1 I >; 338 c*o‘.t«i zon
Jetzt die Definition der +-Operatorüberladung in myWoW.cpp: // myWoW.cpp Character Character;;operator*(Characcer& c} { Character Allianz ('Allianz”, this->hp+c. hp» this->rop*c.Tnp>; *1 cout « this->klasse « 11 und 11 « c.klasse *2 « ” haben eine Allianz gegründeten"; return Allianz; ’J» > "3 Das neue Objekt wird an den Aulruler mrückgegeben •2 KcmUcilUu^abe 7Uf Information *1 Hier vnrd «in neu« Objekt Allianz muift. Der N.imc der Kämplerklaw laut-ct nun „AJJIaru", und die HPs und MPs werden vom aufrufenden ObjcM (*this) mit dem als Parameter übergebenen Objekt addiert. IMftlk) Die erstellte Operatorüberladung kannst du übrigens auch mehrfach anwenden Wenn du drei Helden in eine Allianz verpacken willst, geht beispielsweise auch das: Character Allianz einHcld + gutcHcxc + nochcinHeld; Natürlich bedeutet dies intern, dass die +-Übcdadung zweimal aufgerufen wird. OUTladtn »/cm 839
Glückliche Pärchen So, wie du künftig binäre Operatoren überladen kannst, hast du jclzl erfahren. Natürlich war das Jetzt noch nicht alles, und wir wollen da anknüpfen, wo wir in der Werkstatt aufgehön haben. Du weißt jetzt, dass du binäre Operatoren als Elcmentfunktion oder als globale Freundfunktion schreiben kannst. lEiftUche Auigitx] Bezogen auf unser myWoW-Beispiel sollst du jetzt den +=-Operator so überladen, dass das aufrufende Objekt die HPs und MPs vom als Parameter übergebenen Objekt erhält. Im Gegensatz zur +-Operatorüberladung, die du bereits geschrieben hast, wird jetzt kein neues Objekt angelegt und zurückgcgebcn. tdfar Ce/h*-, //A //A /^A /Zexe.? Ja, das habe ich eigentlich gemeint. Kriegst du das hin? /)/£ /r-t myWoW. h // myWoW.h dass Character { *1 Zurückge^brn wird cm Objekt der Klaw Character. und.iK “1 Hier dddieiert wir die HP* und vom .luhufrnrfen Objekt (linker Operand) mi1 dem der HPs und des Parameters (rechter Operand). public: Character^ operator + (Character&); >2 Parameter (Operand auf 1 der rechten 5e4e> verwenden wir eine Referenz auf ein Objekt der KUsu- Character •1 Jetzt zur Definition des +=-Operators in myWoW.epp: Character^ Character::operator +« (Characteri e) { this->hp+»c.hp; *1 this->mp+«c.hp; *1 340 ZCMN
*2 Information, wer cout « this->klasse « ” erhält die HP/HP von " was von wem erhalt « c-klasse « endl; *2 return *this; *3 •— —*3 Wkhtig, wir wollen ja kein neues Objekt erzeugen und zurückgeben. sondern nur das autrulende Objekt mrt seinen neuen Kräften! Und d.iturgib!'s ji den this Zeiger. Und jetzt noch ein Codeausschnitt als Beweis, dass das auch funktioniert: Character einHeld("Edler Ritter", 20, 5); Character guteHexe("Brave Hexe", 10, 11)5 _ elnHeld+«guteHexe; ’1 Character cinBocscwlcht("Schwarzer Magier", 18, 15); e inHe Id. indenKampf (e inBoesewicht); *2 "1 Huer werden dem Objekt einHeld zu seinen iktuehen HPs und MPs die HPs und MPs vorn Objekt gutcHcxc hirtfuAddiert. *2 Und dm Ende sießt wieder rrul das Gute Sö, wir r» da ♦Cthr Der überladene Opera» !0r wild xwhgrf uhrt "Edler Kater" gegen "Schweizer Mjgker' ZmifWoW tJI er nriir (HPz 2 0/ W; ?) IP 1 Q/W l l ) (in neuer Ke I d 1 * t d-i he» < S<* h w«> F xer Hi y ei tdl er K» Her ( MPt ^O/K^’s 1 f) $rei ft an! U.<i er (MF’ J/fc#’. I 5) greift /in! t dl er Ri t t er ( HTi i 5/: i $) e। ( t an! Schwarzer kitci er verloren! r> e t e r fr-i e r > HR GUT gemacht! Hier wird der überladene + -Operator ausgefuhrt. Prima! * Das hast du OfcTleden von bpT<tor»n 341
Jetzt noch eine weitere Aufgabe mit anderen Parametern: (Schwierige Aufgabe) Füge zu den bereits vorhandenen +- und +=-Überladungen nochmals eine +- und eine +=-Variante hinzu, mit denen du einen intefM-wert zum Objekt dazu kannst, bspw. mit einHeld+= 10 den Integer-Wert 10 zu den HPs vom Objekt einHeld Verwendest du hingegen nur einHeld*10. dann wird der Wert dieser Addition nur zurückgegeben, nicht aber zum Objekt hinzuaddiert (eigentlich logisch so). Die Deklaration in myWoW.h If myWoW.h dass Character { public: *1 Da uns hierbei nur die Addition der HPs des Prüfenden Objektes (linker Operand) m 1 dem Integer jls Par.imeter (rechter Operand} interessiert, gibt diese Operator- Überladung cm int zurück Character operator*(Character!); Character operator+=(Character4c); int operator*(int); Character! operator + (int); *2 >; *2 Bei dieser Vetwendung soll ja der l meger Iah Parameter (rechter Operand} zu den privaten HP^Daten des aufrufenden Objektes (luikcr Operand) hinzuaddierl werden Daher brauchen wir ah Rückgabewert wieder ein Objekt der Klasse. Jetzt die Definition der beiden Operatoren inmyWoW.cpp •1 Hier geben w rd lediglich die Summe vom HP-$Und des aufrufenden Objektes (linker Operand) und des Parameters V (rechter Operand) zurück. fl myWoW.cpp int Character::operator+(int v) ( return this->hp*v; *1 > ’2 Zunächst addieren wir den HP-Wert des lufrufendefi Objekte* (linker Operand) zum Parameter V Character! Character::operator (int v) { ('«Mw opciand) hinmi this->hp+=v; “2 return *this; *3 > *3 Dann geben wir quasi das dufrufendL* Objekt mit dem neuen HP-Wert wieder an weh selbst zurück Natürlich auch hierzu noch ein kleiner Kampf: Character einHeld("Edler Ritter”, 20, 5); Character cinBocscwicht("Schwarzer Magier", 18, 15); if(einHeld* 10 > 18) { ‘1 eir.Helc*=lO; Hier bekommt unser Held 10 HPs hmzuaddtert ‘1 Hier verwenden wir die erste ♦-Überladung. Wir prüfen zuerst, ob bei unserem Helden eine Erhöhung der HPs um 10 Punkte mehr als die 18 HPs ergeben, die unser Bösewicht hat. Hier solltest du natürlich eine Zugriffs- elcmcnlfunklHNi implementieren, welche den HP-Wert der Objekte zurückgeben kann 342 c«iwi zcmn
einHeld. IndenKainpf (einBoesewicht); "3 > ________________________ eise { *4* - ' “ - einHeld+-10; elnHeld .gecScacus () $ - cout « ” rennt davon\n*r; “3... und stützt skh mit vi*l Selbst- vertrauen in den K^mpf. If myWoW ,h dass Character < ‘4 Abzweigung wird □uigcführt, wenn der ßöicwicht trotz einer HP-Erhöhung um 10 Punkte Immer noch starker Ist. Zwar bekommt unser Meld di’nrwh seine 10 HPs. verzichtet H; < r auf den Kampf und nimmt d «? Seine in du Hand. {Schwierige Awlgibe) Unser Beispiel hat jetzt doch noch ein Problem. Und zwar funktioniert die folgende Operatorüberladung nicht: if(10+einHeld > 18) {•••} Was kannst du tun. damit 10+einHeld funktioniert, wie einHeld+10? Zunächst die Deklaration in myWoW.h public: friend int operator*(intt Characcerfc); *1 >; i Naja, um es kurz zu machen, unser Held stürzt sich in den Kampf, weil er eben mehr HPs als unser Bösewicht hat. Aber ein Happy End gibt es da leider nicht, weil unser Held einfach zu schwach ist, was die MEs betrifft. Game over! ’1 Mi1 dem SchlmicIwDrt friend Ticldrn wir unsere Opefatarubcrladung ah g^ob.Vc Funktion bei unterer Klaw an. Or wte Parameter Ist der Imke und der zweite Parawtcr der rechte Operand. Jetzt nur noch die Definition unseres globalen Freundes: Ine operator*(int 10perand+ Character fcrOperand) { return lOperand ♦ rOperand.hp; } So, jetzt klappt cs auch andersherum. |Brlohnung/l,d»ung| Super mitgemacht und super gelöst. Zwar wurden hier vor- wiegend + und += als binare Operatoren beschrieben, aber das Wissen kannst du natürlich auch auf alle anderen binären Operatoren an wenden. Zur Belohnung darfst du wieder dein „großes" WoW spielen. 0fc*rl*dtn von « r «tor«-n 343
Wenn du Operatoren überladen willst, dir einsam mit einem Operanden verwendet werden (bspw« - oder Jh also die unsren Operatoren, dann musst du ein wenig anders vorgehen. Die Syntax, um einen unären Operator als Elementfunktion zu überladen, sieht so aus: /tückgabereerc (Klassermaßen;i)operator @ (> Der Rückgabewert ist eben das. was er immer Ist. Hinter dem Zauberwort operator musst du das ^-Zeichen durch das Symbol des zu überladenden Operators der Klasse Klassennamen ersetzen. Weil unäre Operatoren nur einen Operanden besitzen, haben diese auch keinen Parameter. SC? iv-ve ic£, rfftS “blabla, ^blublub, “au, ^-weh, /hurra ttSh/. ? (Hi*trrgrMn(frnfo| Aufrufen kannst du diese uoen unären^Prafixoperatoren entweder gobjekt odei eben mit objekt. operator^(). Das (i-Zeichen ersetzt du natürlieh wieder durch das pperatorsym bo I _________________________j____ blabla/^- blublub— ? 344 ZChn
Bin beeindruckt! Das hast du in der Tai sehr gut gesehen. Beim Inkrement- und Dekrementoperator in der Postfix- schrcibweisc (val++ und val--) müssen wir in der Tal gesondert vorgehen, indem wir einen Dummy-Parameter vom Typ int verwenden. Für val+ + und ++val bzw. val- - und --val musst du somit zwei bzw. vier verschiedene Elementfunktionen schreiben. Diese beiden speziellen Posttixdinger wül ich in der Praxis dann noch gesondert behandeln. |Z«Ue<] In der Praxis solltest du Präfixschreibweise der Postfixschreibweise vorziehen, weil hierfür kein temporäres extra Objekt benötigt wird! ObjektrOpcrator@(int), 0 ersetzt du wieder durch das wi IHintrr^rundin&oi Aufrufen kannst du die Postfixdingi dann entweder klassisch mit objektg oder etwas umständlicher Willst du hingegen einen unären Operator mit einer globalen friend-Funktion über- laden, sieht die Syntax etwas anders aus: ftückgabehrert operator @ (Klasacnnamci Operand)t Was der Rückgabewert ist. weißt du ja. Hinter operator verwendest du für @ das gewünschte Symbol, dessen Operator du überladen willst. Well eine f r iend-Funktion ja kein Teil der Klasse mehr ist. brauchen wir noch einen Parameter vom Typ der Klasse, welcher den Operanden repräsentiert. Ot)*rl*d»n von 4p « r «torc-ri 345
Das einsame Leben der einsamen Operatoren Die Spieler deines myWoW Spiels wollen jetzt nicht nur Allianzen eingehen, um die Welt vor dem Bösen zu schützen. Es muss natürlich auch einen Weg geben, die MP-Punkie für den Angriff zu verbessern oder zu verschlechtern, um sich .aufzuleveln'. Und was eignet sich da besser als eine schrittweise Anhebung oder Reduzierung mit den Operatoren ++ und Per gewicht Die Inkrement- und Dekrement- operatoren wollen wir jetzt so überladen, dass damit die MP-Punkte um den Wert 1 erhöht oder verringert werden können. Schwärzer Malier HP: 1p Mp-jJL'- I----------_______d Per dwte Erffcr Rrttdr HP: M> MP^V MP-LEVEL DOWN MP-LEVEL UP Zunächst zur einfacheren Wahl, dem Überladen der Präfixschreibweise ++abc und —abc. 34t IChn
Hierzu also zunächst die Deklaration der Präfixversionen von ++ und // myWoW.h dass Character { public: Character& operator++(); ’1 Characteri operator--(); *1 Jetzt die Definition in myWoW.cpp ff myWoW.cpp 1 ZuerU fuhren wird ie Addition Subtraktion vom ( Operand) um den Wert 1 durch (rufenden Obie Characteri Character::operator++() { mp++; *1 return *this; } Characteri Character::operator—() < if(mp > 0) *3 mp++; *1 return *this; *2 Hier brauchen wir kern temporäres Objekt und geben das aktuelle Objekt (Inklusive dem neuen MP-Wert) als this-Zeiger weder zurück. Ein wenig anders musst du bei der Postfixschrcibwcise abc++ und abc-- vergehen, Aber, sieh selbst. Hier die Deklaration der Postfixversionen von ++ und -- in myWoW.h: // myWoW.h dass Character { public; • Character operator ++(int)j *1 Character operator --(lnt)j *1 '1 Damit das Ganze überhaupt funktioniert, brauchen wir zunächst einen Oumrny’Parameter vom Typ int Ak RiKicg.ibrwrrt mu« jettf ein Objekt der Klasse Character und keine Heferenj hethaHen. Warum? erfährst du gleich OttrHdin von Gpiritortn 347
Also, auf zur Definition der Postfixnotation in myWoW.cpp: ff myWoW.epp Character Character::operator++ (int) { Character tmp(*chis); ♦+nip; *2 return tmp; *3«^ > *1 Hier legen wii mithilfe d« StandardkopierkonUrukton eine Kop.-r warn aulnrlefidcn i* Objekt an, r3. Hier geben wir die Kopie zurück. 0» tun wir, vw*II doch bei de« Rostfixschi,e»bweise der Wen erst nach Auswertung d« Objekt« geändert wird. Ocn Unterschied jwiichen ++abc und abe-n- h.iben wir p bereits bei den Operatoren im Allgemeinen F behandelt, •2 Hier andren wir <fcm We«: <tef ViP-Power. Character Character:ioperator-- (int) { if(this->mp < 0) 'M return *this; *4 Character tmp(*this); --mp; *2 return tmp; *3 *4 Emen MP-Wrrt. der kirlnrf- gleich 0 Ist. brauchen wir nicht mehr zu verringern. neuen Operatoren in Aktion Jetzt unsere I /inain,cpp Hagler", 18# 15); endl; endl; ++cinHeld; einHeld*8ecScacuf?(); cout « endl; --einBoesewicht; einBoesewicht.gctStatusO; cout « einHcld++; elnHeld>getStatvs(); cout « endl; einBoesewicht--; einBoesewicht.gctStatus(); cout « Character einHeId("Edler Ritter'1. 20. 5); Character einBoesewicht("Schwarzer Jar ganz genau! Wenn wir schon einen Operator überladen, dann sollten wir auch versuchen- die Regeln des ursprünglichen Operators zu erhalten, so wie diese auch bei den gewöhnlichen Basisdatentypen enthalten sind und wie man diese eben auch kennt 348 ZtHAj
Am Ende bleibt ein elnsemer Operator Das Prinzip der Operatorüberladung gestaltet sich also auch bei den einsamen (unären) Operatoren recht einfach. Lediglich bei der Portfixschreibweise von abc++ und abc- - musst du ein wenig improvisieren und darauf achten, dass im aktuellen Ausdruck von obj ekt++ bzw. ob j ekt-- noch der alte Wert verwendet wird. Erst beim nächsten Ausdruck sollte sich der veränderte Wen bemerkbar machen. Du hast Recht! Das hätte ich fast vergessen. Für diese Zwecke darfst du den ! -Operator Überladen. (Einfache Aufgabe) Überlade den ! -Operator ab globale Funktion $o, da« eine Überprüfung von !objekt den Wert true zurückgibt, wenn entweder der Held oder der Bösewicht keine HP-Punkte mehr haben false gibt es zurück, wenn der Held noch am Leben ist. Tipp: Der I-Operator sollte dem O>-Standard gemäß einen bool-Wert zurückgeben. So schwer ist das doch jetzt nicht, Gut, ich helfe dir. Hier die globale Funktion: bool operatorI(const Characterfc c) { *2 Gibt true zurück. wenn d e HPs von C kleiner gleich 0 s»nd und der Held somit den virtuellen Tod erfahren h.it. Ansonsten wird false zuruckgcgebcn. • 1 CM?r Funktiunskopf mit ; unsren Anforderungen bool als Rückgabewert. und C ist Referenz auf das Objekt der Klasse Character genauer unseren So, die Implementierung dieser globalen Operatoriibcrladung in myWoW • h kriegst du aber Jetzt doch selber hin, oder? Ja, klar! Hier die Deklaration des l-Operators in myWoW.h als Freund: II myWoW.cpp « • dass Character { OtTliden vQn frpqrHQr»n 319
public: fricnd bool operator](const Charactcrii); >; Primat Einsetzen kannst du diesen !-Operator jetzt, wie folgt: Character einHeld("Edler Ritter", 20, 5); if(!einHeld) { *1 cout « "Carac over!!1\n"; 1 •1 Unser HeM tat keine HRs. jlso Lebenspunkte, mehr und i« somit gestorben1 ICMtthe Au<K«bel Du erinnerst dich sicherlich noch an die Elcmcntfunktion Characte r:tindenKamp f(). in der du zwei Kämpfer im abwechseln- den Schlagabtausch gegeneinander hast kämpfen lassen Dabei wurde ja auch auf kleiner-glekh der Hf j des entsprechenden Helden geprüft Statt dieser Oberprüfung kannst du jetzt auch den !-Operator verwenden. Ändere die Funktion so um, dass du den |ieu überladenen '-Operator stattdessen verwendest. Ich versuch’s! Hier die neue Elementfunk- tion indenKampf() in myWoW.cpp: // myWoW.h void Character:zindenKampf(Character &cl) while(l) { thls->getStatus(); cout « " greift an1\n’'; cl .hp-«this->tnp; if(lcl) { *1 cout « cl.klasse « *' break; 1 cl .getStatus(); cout « " greift anl\n thi$->hp-*cl.mp; if( t *thia) { *2 cout « this->klasse « break; > { *1 Hier ubefprufen wir die HPs von unserem Gegner. <fcn der Aufruhr der Elemenllunktion ruent verhauen dirf hat verloren!\n"; *2 Und hbcr werden die HPs des Aufrufen der Hementfunki»on überpfOft. der jrtzt Haue von seinem Gegenüber (Parameler} kriegt hat verloren\n"; 1 f Prima gemacht! Du hast jetzt gesehen, dass die Überladungen von einsamen Operatoren auch nicht viel schwerer zu schreiben sind als die binaren Pärchen-Operatoren. Gönn dir wieder ein bisschen Pause, oder geh an die frische Luft, bevor du im nächsten Abschnitt die Logik-Operatoren überladen darfst! 360 zcmn
iffobjektl I- objekc2) M-- Weitere Operatoren Überieden Im Grunde hast du jetzt bereits die Prinzipien der Operatorüberiadungen verstanden und kannst sämtliche Operatoren für Objekte (djeiner Klasse schreiben. In diesem Abschnitt will ich dir noch einige spezielle und nützliche Anregungen geben, die du bei deiner zukünftigen Karriere als Operatorvcrleger gut gebrauchen kannst. Operatoren, die du fast immer für (d)eine Klasse(n} überladen willst, sind die logischen Operatoren = iftobjektl ~ und ! =.. Damit kannst du zwei Objekte wie folgt miteinander vergleichen: — Objekt2) *1 Für tekhe Zwecke Mx'ltcit du immer mit dem Übrrl.idri von == und I« grwjppnct wm Damit Logik und Sinn der beiden Operatoren auch zueinanderpassen, musst du bei allen zwei Operatoren bool als Rückgabewert verwenden. .Typenverbiegemimwandler“ überladen Manchmal kann cs auch hilfreich sein, den Cast-Operator zu überladen uni ein Objekt in einen anderen Typ umzuwandeln. So kannst du diesen Operator verbiegen: Kfassemtamen:: operator CONVTYP (); Mit CONVTYP gibst du den neuen Typ an. in den das Objekt der Klasse Klassennamen umgewandelt werden soll. Ofrl«d»n vqn 351
!*chtun^l Du hast es auch richtig gesehen, diese Überladung des Cast-Operators hat keinen k Rückgabetyp im Kopf stehen. Außerdem wird empfohlen, daraus eine const- Elementfunktion zu machen, damit auch const-Objekte konvertiert werden können. Wenn man hei den Klassen String und VCCtor über die Operatoren » bzw. << Daten nach ein bzw. COUt schieben kann, dann muss das natürlich mit jeder beliebigen anderen und selbst geschriebenen Klasse auch gehen. Genau! Aber erst einmal nur in der Theorie, damit du Hintergrund wissen hast, woher oder wohin du was in und um dein Objekt herumseb lebst. Input me ... Die Eingabe von einem Objekt erfolgt über ein. ein ist ein Objekt der Klasse ist re am. welche in <iostream> definiert ist. Über ein schiebst du also mithilfe des »Operators die Daten in dein Objekt (ein » ob j ekt). Damit dein Objekt auch was mit den zugeschobenen Daten anfangen kann, musst du den Operator » entsprechend für die Klasse anpassen. Da außerdem ein ein Objekt der Klasse istream ist und du hier einen Zugriff auf die (meistens) privaten Daten deiner Klasse haben willst, musst du die Überladung als globale Freundfunktion schreiben. Also lautet die Syntax für eine »-Überladung: friend istrcan»4t operator » (istreami ist Klassd Objekt); Von iS kommen die Daten, und nach Objekt schieben wir diese Daten. Verwenden kannst du diesen Operator dann auf dir zwei folgenden, gleichwertigen Arten: ein » Objekt; operator»(cin, Objekt); 362 i zom
Output me ... Ähnlich funktioniert dies auch mit der Ausgabe, nur dass hierbei der «-Operator überladen und stau istream hierfür ostream <=Ausgabe$tream) verwendet wird: friend ostrearck operator « (oscreami os, Klasse^ Objekt); In diesem Fall kommen die Daten von ob j ekt. und wir schieben diese jetzt nach OS. Save IH-nttr^rundintol Beide Operatoren lassen sich so nicht nur zum Einlesen von der Tastatur oder zur Abgabe auf dem Bildschirm verwenden. Diese Überladung kannst du au< einer Datei benutzen $t«i Eingabestream wie : Ausgabestream wie cout und of strcam die gleichen Basisklassei Basisklassen und die Streams für Dateien wirst zum Auslesen und Schreiben in fatt cio brauchst du nur einen ifstream und statt cout ein, ___o_____________ ofstreaai zu verwenden Dass dies funktioniert, liegt darant dass bei ein und ifstreanj bzw Ll ZThfeJ—in vorhanden sm *** itC OUHidin mqa ^piratortn 363
Spezielle Operatorüberiedungen In der Praxis Wenn dir der Abschnitt eben zu theoretisch war, kannst du dir jetzt wieder die Finger wund tippen. Wie bereits kurz erwähnt, brauchst du fast mit Sicherheit bei deiner Klasse Funktionen, uni zwei Objekte auf deren Gleichheit zu überprüfen. In unserer Welt macht man dies gewöhnlich mit den Operatoren == und ! =. Au^jibrl Um da.> Xu'*" ?nd .Bose" bzw. die einzelnen Übjrlctr in pvnctö InhaJ! überhaupt unterschevden ZU IrOnnf n. nmHW war die beiden logischen Opera'Q«pn SS und | = überladen. Schreib die Operatorüberladung für die Klasse Character als Elementfunktion’ Der Vergleich soll auf alle privaten Daten (hp. tnp und klasse) angewendet werden. 364 c«it«i zcmn
Hm, das ist nicht schwer! Hier die Deklaration in myWoW.h: Jetzt die Definition in myWoW.epp: // myWoW.h // myWoW.epp dass Character { privates string klasse; *1 int hp; *1 ------- int mp; *1 public: bool Character: :operator«(Charactcr& c) { return ( (hp c.hp) && (tnp c.mp) && (klasse c.klasse) ); ’1 bool operator" (Characteri); *2 bool operator!•(Characverl); *2 >S ‘ I Dir privaten Daten des Objektes sollen mitcm.indc verglichen werden. ‘2 Die Deklaration der Überladungen. Beide Elementfunktionen geben gemäß dem €♦♦ «Standard einpn Booleschen Wert true oder false Ainxk. Tipp für den !=-Operator bool Charactcr::operatorI(Character^ c) { return ( (hp !• c.hp) || (mp !• c.mp) || (klasse 1= c.klasse) ); ’2 •2 Hier wird true zurückgcgcben, sobald mindestem »In» der dre Angaben des Aufrufen im Vergleich m»t dem Objekt C nicht gleich ist false wird zunjekgegeb* wenn alle Daten Sind, ' 1 Hwr wird true niruck/fc-gcbcn. wenn all» drei B«4ingung«n zutrrfton - also alle Daten des Ausrufers Identisch m>t dem Objekt C sind. Ansonsten wird false zurückgegeben. } C UJ Kode bearbeiten) x-"\ V Anstatt beim Überladen des ! =-Operators mit einem Intermezzo von logischen // Verknüpfungen auszuwerten, kannst du stattdessen auch gleich den überladenen L® -Operator verwenden und den Rückgabewert negieren, Also, kurz und schmerzlos return loperator==(c); Jetzt noch die neuen überladenen Operatoren in Action: Character einHeld("Edler Ritter”, 20, 5); Character einBoesewichtC'Schwarzer Magier", 18, 15) Character klonkrieger(einHeld); if( einHeld ! elnBoesewichc ) *1 cout « "Die Objekte sind nicht gleich!\n"j if( einHeld klonkrleger ) *2 — cout « "Beide Objekte sind gleich!\n"; *1 Hier trifft et nJ. dass die beiden Hddcn nicht vom selben Format sind. • *2 Klemen hl zwar hierzulande Immer noch verboten, aber wir haben cs trotzdem getan Die beiden Helden — haben dieselben Attribute. Yeah. das Imperium schlagt zurück» .Für neuen Kampf du musst sein bereit’* OfrTlJdOn vpr, <Jpor*tar+n 366
"1 Die Deklaration. nirt der du das Objekt der KUsie Character in ein int umwandeln kannst COHSt haben wir wrwendel, damit du hiermit gegebenenfalls aixh COnst-Objektc konvertieren kannst |ti*fjchr Auigabel Überlade den Cast-Operator so. dass du den HP-Wert eines Objektes der Klasse Character an einen Integer zuweisen kannst (bspw intcger=objckt;). Kinderspielt Hier die Deklaration in myWoW.h dazu: fl myWoW.h dass Character < public: operator int() const; *1 >5 JWotiizettHJ J Für den Fall dass du ein versehentliches Casten nach int verhindern willst, kannst du seil C++11 das Schlüsselwort explicit vor der Deklaration setzen. Dann müsstest du explizit die Schreibweise inc val{clnCharakccr} verwenden Jetzt die Definition des Cast-Operators in myWoW.cpp: II myWoW.cpp Character:töpcrator lnt() const { return hp; *1 > •1 Wir geben ledlgtkh das zurück, in das wir imsei Objekt uniwdixfeln wollen. In unserem Fall nur die D.ilen hp Jetzt können wir unser Objekt in einen Integer umwandeln: •1 Die HPs vom Objekt einHeld sichern wir m int backup_HP • einHeld; *1 backup—HP was jetzt Character zwergier( ” Zwerg le r’1» backup_HP, 10>; *2 ja. dank der Überladung des Cast-Operators, möglich fcrt. “2 Die so gesicherten verwenden wir dann ?um Anlegen eines neuen Objektes Was uns Jetzt eigentlich nur noch zur Vollkommenheit fehlt, ist eine vernünftige Möglichkeit, Daten über die Streams ein und cout ein- bzw. auszugeben. Hierzu müssen wir lediglich die Operatoren » und « überladen. (S.hwi.rig* Aurg.brt Schreib zwei Überladungen für die Operatoren » und «. damit du künftig komfortabel Daten für unsere Klasse Character von ein einlesen und über cout ausgeben kannst.
{Achtung/Verricht) Denk daran, dass ein bzw. cout Objekte der Klasse istream bzw. ostream sind und somit nicht auf die privaten Daten der Klasse Character zugreifen können. Du kommst hier also nicht um eine freundschaftliche Funktion herum! Gut, ich helfe dir! / Eine Musterlösung J der Deklaration in myWoW.h könnte so aussehen: II myWoW*h dass Character < publici friend istreant friend ostreaafc >; •1 B-eide Ope/atofubcrl.idungen werden als globale f r iend- Funktionen deklariert operator »(istrearafe, Characteri); *1 n operator «(ostreaenfc, const Character&J; ’1 Etwas schwieriger gestalten sich dann die beiden globalen Funktionen, um die Operatoren » und « zu überladen weil da doch etwas tiefer greifende Stream-Kenntnisse nötig sind: istreami operator cout cout cout return is; > « “HP « “HP « “Klasse 2 -i »(istreami is, Character^ c) { i » i« : is : "; is » c>hp; *1 *— » c.mp; *1 » Ceklassc; *1 *1 Orr linke Operand is ist mit dem Eing^beMrejm (gewöhnlich cini verbunden. Im rechten Operanden c findest du unter Objekt, wo wir petri über dir Eingabe is dir emgrlesenen Werte in die prwjtcn Ditcn des Objektes Kht-rbcn (bspw ein » Objekt;). '2 Al$ Rürkgibewert wird der EirtgjbfiUe^rn zurückgegeben. ostreami operator«(ostreami osF const Character^ c) { os « c>klasse « " (HP:ir « c.hp « n/MP:w « c-ffip « *3 return os; ‘4 } ‘3 Ähnlich ul die-, be* «-Operator, wd OS mi1 dem Ausgabe>trc.im. dem linken Operanden {hier mrrürns COUt? verbunden l$t, dK* privaten Daten au$ dem Objekt C zugwteckt bekommt und n^<h OS iuiglbt (bspw. cout « objektf) *4 Zunuckgegeber* wird der Ausgabesvein*. O^erUdm von frpirjtortn 367
Iwotiil Ich habe bereits erwähnt, dass die Ein- bzw. Ausgabestreams nicht zwangsläufig mit ein und cout verbunden werden müssen. Hier kannst du auch Datei-Ein- und Ausgabestreams verwenden. Jetzt klappt es auch mit der üblichen Ein* und Ausgabe mittels ein und cout: ’i Hier Wird ein Objekt der Klasse Character jnjgelegt. Character einHeld("'Edler Ritter”. 20. 5); M Character mehrHcldcn einHeld; *2 cout « mehrHelden; *3 ein » mehrHelden; ’4 cout « mehrHelden; *5 •3 Dank der Übe! ad ung von « in unserer KUsve könnet wir jetzt dir Daten des Objektesan cout schieben und ausgeben. *5 Hier findest du den Bewein* da» die Eingabe unserer Outen funktioniert hat. ‘2 Nun wird das Objekt it dem Standirdkojxer- konstiukior kop#ert ev solltest du viclletcfit mal den Standard’ nsvuktor sdirdbrn rf M Auch das Einlesen funktioniert dank d<5 überladenen >> Oper.itQfS Hier t.rh eben wir die Emgabc von ein m dir privaten Daten des Objektes. Unsere Operatoren << und » bei der Ausführung: << urtd >> wwd«A UbttUdcrt fiiHrr h.irr > JnujWoW Cir neuer t^l J • *t dabeii Edler K«tler U*’i IC/W 1 .11 r Kl t I r Ml- 2 <? >.? cout « aehrltaldMi. HF* 4 > O * O eia » MhrBtite; kJ < Zwer^i er Zwctrjet Mr SO. K*' L” cwt « thrBtlifea. n V ♦ f r ts.se r S | JAchtüfl£/V«ffichU Für den Fall, dass du einen Objektzeiger für« bzw. » verwenden willst, kann es hilfreich sein, auch dafür eine Operatorfunktion zu schreiben. Natürlich kannst du stattdessen auch den Zeiger dereferenzieren. Das Ding mit dem Objektzeiger: Character *heldenPtr new Character("Kriegulde", 40, 20); cout « *heldenPtr; "1 i »» ’1 AM einer ▼ Dorofofinztorung kannst du ddn Objektiver j jsgebeft Willst du hingegen Folgendes machen cout « hcldcnPtr p "1 Hierfui ne weitere gl< OperalcMfunfc Uil K’S? du ple f riend- Ron schreiben. 36B ucit«i zcm*
Eine Version für einen Objektzeiger ist schnell erstellt: ostreaml operator«(ostreami. os, const Character* c) { os « c->klassc « " (HP:" « c->hp « "/MP:" « c->tnp « ")\n"s return os; Dass du diese Version noch In der Klasse Character als f ricnd deklarieren musst, brauche ich ja. glaube ich, nicht mehr zu erwähnen. (fcclohnungl Unglaublich gut, wie du hier mitgemacht hast. Ich weiß, der Abschnitt war sehr dicht gepackt, aber da waren ein paar Dinge dabei, die du fast immer im Leben wirst brauchen können. Das ist deine Belohnung: < V*rdAj«*t, & ist d®r Ketchup . > OUrUdin von flpirjtoMh 359
Spezialitäten auf dem Sofa Jetzt hast du eine Menge Stoff zum Thema Operatorüberladung hinter dir. und trotzdem könnte Ich dir noch eine ganze Menge dazu erzählen. Daher will ich dich in diesem letzten Abschnitt noch mit einigen Spezialitäten traktieren, für die es ganz nützlich sein kann, etwas Hintergrundwissen zu besitzen. Also, anstatt die Füße hochzulegen und alles nochmals im Resümee durchzugehen, solltest du diesmal (ausnahmsweise) deinen Laptop mit aufs Sofa nehmen und ein paar Überstunden machen. Funktlonaoblekte Toller Name, nicht? Wenn du den Funktionsoperator ( ) überlädst, wird das Ergebnis Funktionsobjekt genannt. Damit kannst du das Objekt quasi wie eine Funktion verwenden. [Zettel] Alte C-Masochisten kennen etwas recht Ähnliches als Funktionszeiger. Bezogen auf die Klasse Character könnte ein solche (als inline) definierte ()-Überladung aussehen, wie folgt: int operator() (const Character &c) { return mp-c.mp; > Der Rückgabewert ist hier int. und als Parameter erhält die Funktion das Objekt C. genauer eine Referenz darauf. Zurück- gegeben wird hier der MP-Wert des aulrufenden Objektes, 3B0 ZCMN
minus dem MP-Wert des Objektes C. Natürlich musst du hier nicht zwangsläufig ein Objekt als Parameter verwenden. Das hier Ist eben nur ein Beispiel. Du kannst durchaus andere Typen als Rückgabewert oder Parameter verwenden, wenn es nützlich erscheint. Auch kannst du funktionsmäßig mehrere Parameter verwenden. Zum Beispiel: Character einHeld("Edler Ritter”, 20, 5); Character einBoesewicht("Schwarzer Hagler'1, 13, 15); cout « einHeId(eltiBoesewleist) « endl; *5 *1 Hier vehst du dat Fun$d»onwt)j<-kt bei der Ausführung. Das Objekt einHeld wird mit einßoescwicht als Parameter aufgerufeti Zu«ückg*geben wird der Weit-10 (CinHcld MP-Wert 5 mirnn einßoscwicht, MP-Wert • 15). Indexoperator [ ] überladen Auch der Indexoperator kann überladen werden. Dies wird bspw. gerne verwendet, um bei Dingen wie einer ganzen Liste von Daten via Index auf die einzelnen Elemente zuzugreifen oder um ein assoziatives Array zu definieren, das Zugriffe auf eine Klasse wie einHeld ( "SuperheroM ] (ähnlich wie map aus der STD ermöglicht. Der Reiz, so etwas zu schreiben, mag vielleicht aus Experimentierfreude ganz schmackhaft sein. Bedenke aber, dass C++ hierfür mit der Standardbibliothek zum Mitnehmen fertige und vor allem lang erprobte Spielsachen anbietet, bevor du das Rad wieder neu erfindest. Die Syntax, um den |]-Operator als nicht- statische Elementfunktion zu überladen: Rückgabewert Kiassennamen::operator [] (int Index); Der Rückgabewert ist in der Praxis beim Indexoperator meistens eine Referenz auf das entsprechende Element und eher selten das Element selbst. Der Klassennamen wird für die Klasse verwendet, für welche du den ( ] Operator überladen willst. Und mit dem Parameter index gibst du den Index des entsprechenden Elementes an. new und delete überladen Praktisch könntest du auch die Operatoren new und delete zum Anfordern bzw. Freigeben von Speicher überladen. Allerdings muss ich dazu erwähnen, dass dies nicht ganz so einfach geht, wie du es von anderen Operatoren hcr kennst. 0b«rl4d«n von «Jpiritcrth 381
Eigentlich nur aus Geschwindigkeit« und/oder Speicherplatzgründen macht cs Sinn, eine selbst geschriebene dynamische Speicherverwaltung für die Klasse zu schreiben. Allerdings muss sich dieser Mehraufwand dann schon wirklich lohnen. Die Gefahr einer fehlerhaften Oberladung ist bei einer unbedachten Implementierung sehr groß und kann das ganze Programm unbrauchbar bzw. instabil machen. Wenn du dir nicht wirklich ganz sicher bist, was du tust, dann solltest du new und delete nicht überschreiben. Wenn du aber weißt, was du tust, musst du folgende Regeln beachten, sobald du new und delete überschreiben willst: - Der erste Parameter von new muss immer der Typ size_t sein, Ansonsten darf der Operator new durchaus noch weitere Parameter haben, delete hingegen hat einen Parameter vom Typ void*. kann aber auch einen zweiten Parameter vom Typ size_t für die Größe des Objektes enthalten. - Da. logischerweise, beim Aufruf von new noch kein Objekt erzeugt werden kann, Ist new immer eine statische Elementfunktion (Static). - Der Rückgabewert von new muss immer ein void-Zeiger (void*) sein, delete hingegen muss nur void als Rückgabetyp haben. • Für jede Klasse kannst du durchaus mehrmals new überladen, aber von delete darf es nur eine Version geben. Soll eine Klasse, für die du new und delete selbst geschrieben hast, auf die globalen Operatoren angewendet werden, brauchst du nur den Bereichsoperator davorzusetzen (s tnew und :: delete). Außerdem musst du noch unterscheiden zwischen dem Reservieren von einzelnen Daten. Feldern. Objekten und Objektfeldern. - Vl 8 <Ze««el] Der Typ sizc_t wird vom Compiler zur Speicherreservie- rung vorgegeben und ist gewöhnlich ein typedeL entweder vom Typ unsigned int oder unsigned long IBtlOhAUHfj'lAtlHtgl Toll, dass du bis jetzt durchgehalten hast! Ich weiß, am Ende war es sehr theoretisch, und mit new bzw, delete hast du noch einen ziemlich dunklen Pfad durchschritten. Gerne hätte ich dir noch etwas mehr dazu geschrieben, aber irgendwann muss mal Schluss sein. Wenn du mehr erfahren willst, solltest du deine Suchmaschine fragen, oder wir treffen uns mal auf ein Bier und sprechen darüber. Zur Belohnung nimmst du dir am besten einen Tag frei und spielst WoW und gönnst dir ein Bier oder ein Glas Wein (oder auch 'ne Limo). Der nächste Abschnitt wird nämlich wieder etwas komplexer! 382 c*o;t«l ZCmx
—tu-— Abgeleitete Klassen Schrödinger macht sein Testament Das Thema Interessiert Schrödinger brennend. Er hat selbst noch eine Tante, von der was riiberkom- men könnte. Schnell merkt er allerdings, dass In C++ niemand sterben muss, um etwas zu erben. Irgendwie scheint In C++ die Erbschaft eher von biologischer Natur (Vater+Mutter = Sohn) zu sein, well es hier auch eine Mehrfachvererbung gibt.
Erben ohne Erbschaftssteuer Jetzt kommen wir zu einer weiteren Säule der OOP: der Vererbung (oder einfach nur abgeleitete Klassen). Allerdings hat die Vererbung in der Programmierung eher weniger mit dem klassischen Fall .Tante Helga ist gestorben, und wer kriegt jetzt die Kohle" zu tun. sondern eher mit dem genetischen .das vorstehende Kinn haben sie von Ihrer Mutter'. Di* M*r*ibung b*i d*r Rregrammiwurtg hit mit dem H«uwuchlrickefi von Geld«fb«i> zu Iw als mit der £*n*liuh*n V*f*tburlg- Es gibt zwei wirklich sehr gute Gründe, eine solche Vererbung (abgeleitete Klassen) einzusetzen: Wenn du Daten oder Elementfunktionen aus mehreren Klassen in deinem Programm verwendest, kannst du diese dank einer Ableitung in einer eigenen Klasse verwenden. Genauso kannst du die zusammengefassten Daten und Elementfunktionen wieder in einer anderen Klasse weiterverwenden. Das ist 'ne tolle Sache, weil du den Quelicode für diese Klasse lediglich einmal schreiben und überprüfen musst. Richtig geschrieben, kannst du eine solche Klasse jederzeit In anderen Projekten einsetzen. 384 tif
Selbst wenn du keinen Code von der Basisklasse hast und lediglich die Headerdateien vorhanden sind, kannst du diese Klasse trotzdem ableiten und die Daten und Elementfunktionen der Basisklasse In deiner neuen Klasse verwenden und um weitere Daten und Elemcntfunktioncn erweitern. Scdc£t ^*SCe s A'&sCc fv4 o6c^i +>f«s ^ed^- Objejtf OfytJtf ✓4»-^ £<4—, ct-^ ‘v'/t Ja. kann man so sagen. _________ (ZMM| Ein Objekt der abgeleiteten Klasse ist auch immer ein Objekt vom Typ der Basisklasse. Die abgeleitete (vererbte) Klasse hat somit zur Basisklasse immer eine IST-ßeziehung. Die IST-Bejiehuftj: Atomkraft IST rin Stm, Windktaft IST Auch ein SUMi, und Sölar- entqjkt IST ebenfaMf ein Stnim. Strom itr-llt hier die Baiij&laue dar. und Atomkraft. Wnadkraf« und Soluenergie wurden jeweilt davon abgeleitet. ibgaleateta Claner 818
dass Klassennaoen : Zugriffsrechte Basisklasse { II Definition von Klassennamen >3 Der Klassennamen ist die Klasse, die du im Augenblick definieren willst. Die Basisklasse ist die Klasse, von der du Daten und E lernen tlün kl ionen an Klassennamen verer- ben willst. Außerdem kannst du die Zugriff Srechte auf die Mitglieder der Basisklasse festlegen. Hierfür stehen dir die Schlüsselwörter private, protected und public zur Verfügung. Die Rechte verhandeln wir aber nochmal in einem extra Abschnitt, okay? dass Windkraft : public Strom { *1 II Definition der Klasse Windkraft iHäftlwtwdinM Viele beschweren sich hierzulande imnjer. dass wir die Voneiter sein müs- was eine saubere Umwelt betrifft. - meistens ein Vergleich zu den größten Drecksstaaten gezogen wird, stimmt das so1 allerdings nicht ganz. In Deutschland "wrd immer noch zehnmal so viel CO. *1 Hier <fcfirn<nt du < ir*c Klaue Windkraft, wekhe von rtrr Basis- klasse Strom abgeleitet wurde Durch das publ ic Zugriffsrecht kannst du in der Klasse Windkraft alle publiC-Mitglicde.' der Klasse Strom verwende a. ausgestoßen als bspw. bei unseren Nach- barn in Frankreich! 3«5 («pitti clf
Ewig schleichen die Erben Wenn wir schon dabei sind, wollen wir in der Praxis auf der Stromschiene weiterfahren und ein bisschen hinter die Kulissen der Sirüinversorger sehen. Zur Vorbereitung wol- len wir daher zunächst einmal eine normale Klasse Strom schreiben, welche künftig unsere Basisktasse darstellen wird. Damit ich den Code nicht auf mehrere Dateien auf- teilen muss, findest du auch gleich alles in der Klassendelinition implizit als inline definiert. Hier also unsere Klasse Strom komplett in einer Datei ström,h verpackt: II ström.h linclude «iostreaen» linclude <string> using namespace std; lifndef _STROM_H Idefine STROM H z/1 Hier fiixJest du die privaten Daten. d»e von außen nicht etrekhtiar sind (in 0*11 würde lieb hier auch gleich eirw direkte Initiahiiefungder Klawcnrlemente Anbieter») ... *2 ... und hier d e öffentli- chen ElementFunkiionen und Konduktoren. dass Strom { private: *1 string quelle; unsigned int KWh; public: *2 StromO :quelle (MW) ,KWh(O> {} Stromfstring q, unsigned k):quelle(q), KWh(k) {} void secQuelletconst string &q) { quelle = q; > void setKWh(unsigned int k) { KWh = k; } string getQuellef) const {return quelle;} unsigned int getKWhf) const { void printO const { cout « "Stroaquelle : ” « cout « "KWh : " « > >; lendlf öGjdtf Ode*,/- return KWh;} getQuelleO « gctKWht) « endl; endl; ci«n*n 367
Ja, natürlich kannst du diese Klasse in einem Programm ver wenden. Ist ja schließlich eine vollwertige und gewöhnliche Klasse, wie du diese bisher überall im Buch verwendet hast! (Code bearbeiten) Auch wenn es dir bequem erscheint, die Listings von der DVD zu verwenden, empfehle ich dir (eigentlich immer), die Listings selber zu schreiben, weil du dabei beim Abtippen viele Fehler machst und dabei auch was lernen sollst! Ein großer Stromkonzern beauftragt dich jetzt, eine Klasse für Atomkraft zu erstel« len, welche Strom nach Kilowattstunden verkauft. Hierzu sollst du noch die Ent- stehungskosten in Cent pro Kilowattstunde und den CO,-Ausstoß pro Kilowattstunde mit einberechnen, Da wir die Kilowattstunden schon in der Klasse Strom definiert haben, brauchen wir ja nicht alles neu zu erstellen und verwenden unsere Klasse Strom als Basisklasse. So sieht unsere Planung aus: Atomkraft bäte« (private): Quelle für de« Strom ♦ Kilowattstunde« (KWk) Fu«ktio«e« (public): _ * Quelle setze« u. abfrage« * KkJk setze« u. abfrage« * Alles formatiert ausgeb«« dass Atomkraft r public Strom bat«« (private): + Go2-Ausstoß pro KNk + Koste«: Ce«t pro Kklk Fu«ktio«e« (public): + Co2 pro KWk setze« u, abfrage« *• Cewt pro Kklk setze« u. abfrage« * Alles formatiert ausgebe« 308 CLf
Du findest jetzt hier schon mal den Quälcode für atomkraft.h, in den wir auch wieder alles inline definieren. Allerdings werden hier noch viele Fragen offenbleiben, welche wir auf den nächsten Seiten beantworten werden ff atomkraft.h linclude <iostrcam> linclude <string> linclude "ström.h" *1 using namespace std; lifndef _ATOMKRAFT_H Idefine ATOMKRAFT H ’i Di Headerdatei unserer Basisktasse muss tMiürlkh auch mit rein Strom { *2 ’2 Hk' f ndr! cinr Vererbung bzw. Ableitung der Klasse St t om statt Durch die public-vereibung hinter dem Doppelpunkt stehen dir alle publlc-Elerente der Basisklasse Strom zur Verfügung. dass Atomkraft : public private: unsigned int Co2KWh; float ctAKWh; public: At omkra ft(uns igne d int string q«**1» Stromfq, k), Co2KWh(co2)> ct^KWh(ct) unsigned int getCo2KWh() const {return Co2KWh;} float getct4KWh() const {return ct^KWh;} void setCo2KWh(unsigned int co) { *4 Co2KWh co; } void setctAKWhffloat ct) { CtAKWh - ct; } void print() const { "5 — cout « ‘*Co2 pro KWh ; * « getCo2KWh() « cout « "Cent pro KWh: ” « getcc4KWh() « Strom::print(); } }; lendif co2=0f float ct=0.0T unsigned int k»0) : {} ‘ J Dass der ktor bri einer abgeleiteten lasse etwas spezieller ist als gewöhnlich, sollte wohl jedem kUr sein. 4 gewöhnliche Elementfunktionen der K lasst Atomkraft endl; endl; "5 Eine gleichnamige Elementfunktion enthalt auch «hon unsere BiiwkUsv Strom Dir redefinierte pr Int-^lenienCunkicofl def Basisklasse Strom wird hierbei einfach überdeckt. *6 Der Zugriff für die formal r pt int-Elemcntfunktion von ssc Strom airerd ngs übe* den Zugriffsoperator Helgen. weil sonst ein endloser SelbstaiHruf von Atomkraft: sprint () erfolgen würde. Das war es dann auch schon! 389
Jetzt ist unsere abgeleitete Klasse Atomkraft bereit zur Verwendung: linclude "atorakraft,h" *1 Atomkraft meilerl; *2 meilerl.setQuelleC"Atomkraft”); ‘3 meilerl.setKWht1000); *3 meilerl,sctCo2KWh(16); *3 meilerl.secct4KWh(2.65); *3 meilerl.print(); ’4 •1 Dir Headerdatei m der alles drlnr$tcht. ist natüdich notig *2 E»n Objekt der Klasse Atomkraft w»rd instanziiert. '3 Die einzelner Werte werden gesetzt *4 Schließlich wird alles auf dem Bildschirm ausgegeben Das Programm bei der Ausführung: |Zel1d| Wie schon erwähnt, sind jetzt wohl ein paar Fragen offengeblieben. Für dich ist jetzt erst mal nur wichtig, dass du weißt, wie der Vererbungsprozess vor sich geht. Oas Kleingedruckte behandeln wir später., 370 uoitti cif
Damit keiner leer ausgeht So, jetzt weißt du schon mal. was cs mit der Vererbung bzw. Ableitung von Klassen auf sich hat. Im Grunde ist es ja nur eine Technik, um bereits vorhandene Klassen zu erweitern. w Itiaf.ich* Aufpbd Im Beispiel ist eine fertige Klasse Birnen_OS_X vorhanden. Jemand anderes schreibt eine Klasse Microwcich_No8 und möchte die öffentlichen Mitglieder der Klasse Blrnen_OS_X in seiner Klasse Microweich_No8 benutzen und erweitern. Kannst du ihm helfen? Mach eine Ableitung daraus: If Vorhandene Klasse, welche wir erweitern wollen: dass Birnen_OS_X ( /* „ */ }} ff Klasse die wir gerade neu schreiben: dass Microweich No8 { /* ... */ }t dass Microwcich_No8 : public Birncn_OS_X { /* ... */ ); Prima! Das war gar nicht so schlecht. Was ich hier allerdings noch erwähnen sollte: Es ist natürlich auch möglich, dass eine Klasse eine abgeleitete Klasse, also auch eine Basisklasse, sein darf. Wie das gemeint ist: dass BirnenOSX { /* _ */ }; dass Hicrowcich_Ko8 : public Birnen OS X { / dass Kaiserpinguin : public Microwelch_Ko8 { *1 Hier bl d r KliiVf Microwcich_No8 sowohl Kiasst als auch ein# BjuHMisse ... */ }; *1 Microweich__No8 tst h t-rbci /*...*/}; *1 yo<i Blrnen_OS_X abgeleitet und furniert gleichzeitig alt Batrtkl.ttM? für die KUsvr Kaiserpinguin Klaaaew tTI
Falls du es immer noch nicht kapiert hast: K«u«q So ein Ableiten von einer Klasse zur anderen wird häufig ber Klassenbibliotheken von grafischen Oberflächen verwendet. Da wird vererbt bis zum geht nicht mehr! Erben verboten Mit C++11 kannst du jetzt auch das Weitervererben verbieten. Hierfür brauchst du lediglich die Klasse mit dem neuen Identifier final markieren: *1 Dank dem Identifier Önal h»cr i$t . dass Blrnen_OS_X final { }; *1 dass Microweich_No8 : public Birnen_OS_X { H ’2 1Ei«fache Aufgabe] Zwar wurde das Thema noch nicht direkt behandelt, aber beim Ableiten von Klassen kannst du Zugriffs rechte vergeben. Mit welchen Schlüsselwörtern kannst du das machen? *2 diese Ableitung rudil mehr mosch’ Oer Compiler qmtterrt mit einem h?hlöf beim Übersetzen1 protected. IftdotauncAtaungl Ganz genau! Jetzt hast du wieder Zeit, ein bisschen Luft zu holen, ehe du dich mit dem Kleinkram rund um die Vererbung rumschlagen musst- Ich empfehle dir. nur eine kurze Pause mit einer Tasse Tee oder Kaffee zu machen. Und wenn deine Lunge danach verlangt, kannst du natürlich auch deine Teerstraße aus* bauen und deine durchschnittliche Lebenserwartung um 12 Minuten verringern. 372 cif
Keine Sorge. Schrödinger! Für das Kleingedruckte brauchst du keine Lupe. Bei uns wird die Vererbung für normale Augen lesbar abgedruckt, und cs gibt keine hinterlistigen Fallen. Ein ganz wichtiges Thema sind die Zugriffsrechte, welche du ja hinter dem Doppelpunkt der zu beerbenden Klasse mit public, private und protected setzen kannst: dass AbgeleiteceKlasse t ZUGRIFFSRECHT BasiskLaase {); Und das kannst du damit bewirken: public: Die Ableitung wird am häufigsten verwendet. Damit erhält unsere ab- geleitete Klasse dieselben Zugriffsrechte wie die Basisklasse. Dein Großvater, dein Vater und du haben somit immer dieselben Zugriffsrechte. Von außen darf hier über das Objekt jederzeit auf Mitglieder im public-Bereich zugegriffen werden. " private: Bei einer privaten Ableitung werden alle geerbten Mitglieder aus der Basisklasse In der abgeleiteten Klasse zu privaten Mitgliedern, Zugriff auf pub- lic und protected innerhalb der abgeleiteten Klasse ist jetzt nur noch Mit- gliedern der Basisklasse möglich. Dein Vater könnte also noch auf die public und protected Mitglieder deines Großvaters zugreifen. Würdest du allerdings, als Erbe deines Vaters, auf deinen Vater zugreifen wollen, kannst du das nicht mehr, well Ja deine Basisklasse (dein Vater) zuvor alles von seinem Großvater (seiner Basisklasse) Geerbte aut private gesetzt hat. Damit bist zu praktisch enterbt! Auch von außen ist kein Zugriff mehr auf den public Bereich möglich. ** protected: Bel einer protected Vererbung werden alle Mitglieder der Basisklasse in der abgeleiteten Klasse zu prot ec ted-Mitgliedern. Innerhalb von Klassen ändert sich dabei im Gegensatz zu public nicht viel. Es kann nach wie vor auf die protected- und public-Mitglieder der Basisklasse zu gegriffen werden. Allerdings ist es jetzt von außen nicht mehr möglich auf dir public-Mitglieder der Klasse zuzugreifen. 373
|M«ntrrgrundin*o| Für den fall, dass du es nicht verstanden hast: Das Erteilen der Zugriffsrechte hat eigentlich erst bei mehreren abgeleiteten ! Klassen einen Effekt. Die erste abgeleitete Klasse hat immer den vollemZugriff auf alle public- und protected- Mitglieder der Basisklasse, Du brauchst dir erstmal nur die public-Vcrcrbung zu merken, denn die wird fast immer verwendet. Wenn du soweit bist, dass du die anderen Fälle benötigst, dann wirst du es schon merken. Zu besseren Übersicht habe ich dir hierzu eine Grafik erstellt, welche dir demonstrieren soll, was bei einer public-, protected- und private- Vererbung vor sich geht: f r*ie CuAk, d-* ifrigt, wat vvretbl wird- wenn du eine Abteilung ah public protected od<f private defclarfem 374 tapHal Elf
Hierzu ein Codesdmipsel, mit dem du die unterschiedlichen Vererbungen anhand der Fehlermeldungen deines Compilers {die du hoffentlich verstehst) testen kannst: dass Basisklasse { private : int pri; public : int pub; protected : int pro; ’1 Zum Testen muwt du nir dir Ttxtlolge ZUGRIFFSRECHT durch d>r ScWÖSK*W>rter public. protected oder —v private Austauschen und lesen. worüber s»th der Compiler beschwert, wenn er darauf hirrwe’ht. dass er kernen Zug;»ff mehr hat. : int pri; : tne pub; dass UnterKlasse : ZUGRIFFSRECHT Basisklasse { *1 // Zugriff auf alle public und protected II von Basisklasse immer möglich >; dass UnterUnterKlasse : public UnterKlasse { public; void initO { pri = 1; pub 2t pro = 3; int mainO < UnterUnterKlasse obj; c6_. l/cjcjk— obj.pri 1; obj.pub 2; obj.pro - 3; Hier gilt dasselbe wie für die Eletnentfunktionen. Die abgeleitete Klasse erbt auch alle nicht privaten Operator- überladungen aus der Basisklasse. Mit einer Ausnahme: Wenn in der Basisklasse der —Operator überladen wurde, wird dieser niemals an die abgeleitete Klasse weitervererbt! 375
niemals Neu in C++11. aber wird leider kaum noch von einem Compiler unterstützt. ist die zukünftige Möglichkeit, die Konstruktoren der Basisklasse an die abgeleitete Klasse weiterzuvererben. Damit Lis« es sich künftig einiges an Schreibarbeit ersparen: dass Papa { public: Papa(short alter) { cout « alter « endl; } Papa(string nacnc) { cout « namc « endl; } ); dass Sohn : public public: using Papa::Papa; Papa { Sohn(float schnitt) { cout « schnitt « endlj } Sohn{45); // Papa::Papa(45) Sohne{"Dieter"); // Papa::Papa("Dieter"); Sohn{123.123}; // Sohn::Sohn(123.123); Wenn die abgeleitete Klasse eigene Variablen enthält, kann es passieren, dass sie durch diese neue Vererbung nicht initialisiert werden! 37< uoiui cif
Das Kleingedruckte in der Praxis Zugegeben, das mit den Zugriffsrechten ist anfangs ein ziemlich chaotisches Wirrwarr. Aber nach ein paar Übungen dazu sollte es keine unüberwindbare Hürde mehr für dich sein! (Einfache Autgab*J Im folgenden Codeausschnitt haben wir die Klasse Mama von der Klasse Oma mit private-Zugriffsrechten abgeleitet. Die ßasisklasse Oma enthält dabei drei int-Variablen, welche alle in unterschiedliche Zugriffsrechte gesteckt wurden. In der Klasse Hama wird dann über die öffentliche Elementfunktion init_ mama () versucht, alle drei Werte zu initialisieren. Kannst du auf Anhieb sagen, welche der drei Initialisierungen eine Fehler- meldung des Compilers hervorrufen wird? Hier der Codeausschnitt dazu: dass Oma { private t int prl; public : int pub; protected : int pro; >; dass Mama : private Oma { public: void lnit_raaroa() { pri - 123} *1 pub = 345; *1 pro - 567; ‘1 > >; 1 Wekhe Zuweisungieo) sind hier falsch? Yeah! Voll ins Schwarze! Aberda will ich jetzt noch einen draufscizen. Hinhchr Aufgabe] Vom eben erstellten Codeausschnitt, bei dem die Klasse Mama mit private-Zugriffsrechten von der Klasse Oma abgeleitet wurde, soll jetzt noch eine Klasse Tochter von der Klasse Hama mit public-Rechten abgeleitet werden. In der Klasse Tochter findest du eine Elementfunktion init tochterf), welche wiederum versucht, alle drei int-Variablen der obersten Basisklasse Oma zu initialisieren. Bei welchen der drei Initialisierungen wird jetzt der Compiler eine Fehlermeldung ausgeben? CUll»r> 377
Hierzu noch ein weiteres Häppchen Code: dass Tochter ; public Mama { public: Oma ct vi Mama void init_tochter() { pri - 3211 *1 hier jeut ülsch? Mama private. Jawohl! Richtig! Du bist der Zugriffs-Rocker. Mach weiter so! Jetzt fehlt uns eigentlich nur noch ein Beispiel zu protected. Hierzu verwenden wir wieder einmal unser Atomkraft Strom Beispiel. Mama rtv-j Tochter, *^»$1 Tochter privatc^/^i/A^ezstlLr ^4/ SCJAH// 4^— Z^yrff fAl—. lflt *-v^ If ström.h dass Strom { private: string quelle; *1 unsigned int KWh; *1 15 Hierzu zunächst die Codeausschnitte von ström, h und *1 private Datan der Biiiskla«« Strom atomkraft.h. die du brauchst^ um das Problem zu lösen: II atomkraft.h dass Atomkraft : public Strom { private: unsigned int Co2KWh; ‘2 float ctAKWh; *2 public: r2 private Daten der abgeleiteten Klasse Atomkraft void print() const { cout « "Co2 pro KWh : * « Co2KWh « endl; *3 cout « "Cent pro KWh: " « ctGKWh « endl; ‘3 cout « "Stromquelle : " « quelle « endl; *4 cout « "KWh » * « KWh « endl; *4 > }; 378 Kapitel ELF ’3 Das klappt ohne Probleme. "4 Der Zugriff auf d«e privaten Daten der 0asiiMai*e führt zu einer Fehlermeldung
(Schwierige Aefg-ibrl Jetzt zur Aufgabe! Was kannst du tun, damit du trotzdem auf die privaten Variablen quelle und KWh der Basisklasse Strom In der Elementfunktion printO der abgeleiteten Klasse Atomkraft zugreifen kannst, ohne eine Elementfunktion dafür zu verwenden? Okay, hier helfe ich dir wieder! Die Lösung ist im Grunde recht einfach. Du brauchst lediglich die privaten Daten der Basisklasse Strom als protected zu deklarieren: lI strom»h dass Strom { protected: *1 string quelle; unsigned int KWh; }| •1 Mithilfe von protected können jetzt auch die ab- geleiteten Klassen auf diese Daten direkt zugreefen. (Woliri Wenn du nachträglich die Zugriffsrechte auf protected setzen musst, so ist das meistens ein Zeichen von unbedachtem Klas- sendesign Bedenke, dass du mit protected die Datenkapselung ein wenig aufweichst1 Probleme konnte dies machen, wenn du protected nachträglich wieder entfernst und somit einige Elementfunktionen ihren Dienst verweigern konnten, in unserem Beispiel bieten sich als Alternativen ja öffentliche Elementfunktionen an, um auf die privaten Daten zuzugreifen. IZ'U«I| Prima, jetzt hast du ein paar Kilo Zugriffs- rechte extra gelernt und kannst dich ent- spannter an die Sache machen Wenn du hier alles kapiert hast, hast du diesbezüg- lich nichts mehr zu befürchten. PAUSE 379
So macht erben langsam Spaß F So, jetzt wolk-n wir die Zugriffsrechte nochmals schon in Ruhe an uns vorbeiziehen lassen. Da ist am Ende doch eine Menge zusammengekommen, weshalb ich dir hier gleich nochmals das wichtigste stichpunktartig aufzählen will: "• Mit public, protected und private hast du drei Zugriffs- rechte kennengelernt, mit denen eine Klasse von der Basisklasse erben kann, private dient dem Zugriff der Klasse selbst, protected wird ebenfalls für den Zugriff der Klasse selbst und von ihr abgeleiteten Klassen verwendet, und mit public erhalt Jeder den vollen Zugriff. Unabhängig davon, welches Zugriffsrecht du verwendest, die erste abge- leitete Klasse hat immer auf alle public- und protected Mitglie- der der Basisklasse einen Zugriff. " Eine abgeleitete Klasse kann niemals auf die privaten Mitglieder der Basisklasse zugreifen. Dafür kann eine abgeleitete Klasse auf die öffentlichen Mitglieder zugreifen, als wären es die eigenen. Mit protected kannst du dafür sorgen, dass nur noch abgeleitete Klassen einen Zugriff auf die Mitglieder bekommen. Alles nicht private der Basisklasse erbt die abgeleitete Klasse. Mit Ausnahme des überladenen Zuweisungsoperators =. der niemals an die abgeleitete Klasse weitervererbt wird! " öffentliche Mitglieder der Basisklasse werden beim Abteilen mit pri- vate in der abgeleiteten Klasse auch privat. lAchtunjI Wenn du beim Vererben keine Zugriffsrechte angibst, wird per Standard mit dem Zugriffsrecht private abgeleitet. (Zettel] In der gängigen Praxis wird meistens eine öffentliche Vererbung durchgefuhrt. Vererbungen vom Schlage protected oder private werden eher selten angewendet. Und wenn du dies dann verwendest, solltest du zumindest kommentieren, warum du das tust, damit auch andere wissen, was du überhaupt tust. 380 tLf
Private Mitglieder durchreichen... Bei eine» private-Vererbung werden Ja alle Elemente der Basisklasse zu privaten Mitgliedern. Bei einer Weitervererbung oder einem Zugriff von außen ist somit: kein Zugriff mehr auf Mitglieder der BasisJdasse möglich. Solltest du jetzt trotzdem (notfalls!!!) in Verlegenheit kommen, auf einzelne private Mitglieder der Rasisldaw zugreifen zu müssen, hast du hierbei nur noch die Möglichkeit, das Mitglied Udurchzureichen\ Du machst quasi eine neue öffentliche Schnittstelle, indem du das Mitglied mit dem Klassrnnamen und Zugriflsoperator an sprichst. Folgender Codeausschnitt soll dir das demonstrieren: dass Oma { public : int pub; *1 }| dass Mama : private Oma { *2 publics using Oma::pub; ’3 void Init^maaa() { pub - 345; a4 > H ’1 pub wird öffentlich lugAngfich in der (kni Jclar.u? Oma neben. ‘2 Durch die private Ableitung wird pub jdzt priv.U in der Kl.wsi* Mama “4 Dieser Zugriff wJbre noch ohne die Zugritfvdckl.iratian möglich gt^vwn a3 Dank diese/ Zugriffsdeklar^tion fluchst du jus pub jetzt wieder ein öffentliche* Ding. dass Tochter : public Mama { public: _ \ void init_tochter() { pub - 543; ‘5 . *5 ... aber dieser Zugriff und,. >5 int main() < Tochter tochter; tochter-pub 123; *6 "6 .. dieser Zugriff wArcn ohne dir neue Zugnffs- dcki.u.)tio<i mit Oma:: pub nicht mehr ohne Beschwerden des Compilers ausgewogen |BdoMung/LMu*g| Ja, Schrödinger, du hast es drauf! Den Abschnitt kannst du jetzt zu Grabe tragen. Nochmal Schnitzel? 361
Einer der großen Vorteile und Hauplvcrwcndungszwccke der Vererbung ist es natürlich. eine bereits vorhandene Klasse um weitere Funktionen und Daten zu erweitern, Wie dies geht, hast du im Grunde ja bereits beim Beispiel der Basisklasse Strom gesehen, welche mit der Klasse Atomkraft durch eine Abteilung erweitert wurde. hole* IprCvdC: * string * wsigHcdl tat Kldk; ____ _ ?untafcto*MtiA Gnxbllc); * sc(QimU*Oj * stfcKWkO, gttKWkl) + StroiviO (Köviftruktor) * pri*K) Strorvi (.private); * uwsiwed ürtt Ce>2K|\lk; * float ^uviktiov»^ (pvblic): *• •tfcCoÄIWkOj gt£CoWWkQ * seict^XNkO, g^Ect^KMkQ * Ato*MkTO.ft(..*) (Konstruktor) * priHtO Atomkraft dass Atomkraft : public Strorw Durch die Ableitung der Klasse At omkra f t von der Klasse S t rom wurden sämtliche öffentlichen Elementfunktionen und Daten (hier keine) an die Klasse Atomkraft weitervererbt. Als da wären die Element! unkt ionen setQuelle ( ). getQuelle (). setKWh(). getKWh(). print ( ) und der Konstruktor Alle diese Mitglieder kannst du jetzt in der Klasse Atomkraft auch verwenden. Auf dir privaten Daten quelle und KWh hast du allerdings keinen direkten Zugriff von der abgeleiteten Klasse. Aber dafür hast du Ja eigentlich auch Zugriffselement Funktionen definiert. Zusätzlich haben wir zu den geerbten Mitgliedern der Basisklasse Strom den Funktionsumfang in der Klasse l omkraf t um die Elementfunktionen setCo2KWh().getCo2KWh()4setct4KWh(). getctAKWh(). print ( ) und den Konstruktor erweitert- Auch zwei neue private Daten sind mit Co2KWb und ct4KWh hinzugekommen. 362 cif
Besonders von Interesse dürfte « hier sein, dass zweimal die Elementfunktion print () implementiert ist. Einmal Strom: :print () und einmal Atomkraft: :print (). Natürlich erbt auch die Klasse Atomkraft diese gleichnamige Eleinentfunktion von Strom Diesen Sachverhalt, der sich Redefinition nennt, wollen wir uns jetzt auch gleich näher ansehen. innerhalb der Klasse Atomkraft bedeutet eine solche Redefinition natürlich, dass das print ( ) der Klasse Strom überdeckt wird. Gleiches gilt auch für gleichnamige Bezeichner bei den Daten - im Grunde ähnlich wie bei den globalen und lokalen Variablen, wo bei gleichen Bezeichnern immer die lokalere Variable den Zuschlag erhält. Bei gleichnamigen Bezeichnern zwischen der Basisklasse und der abgeleiteten Klasse über- Im Beispiel mit der Elementfunktion Atomkraft::print () sieht dies aus, wie folgt: II atomkraft.h decken immer die Daten bzw. Elementfunktionen der abgelei- teten Klasse die der Basisklasse. dass Atomkraft : public Strom ( Wie du dir jetzt sicherlich schon gedacht hast, kannst du nach wie vor auf den gleichnamigen Bezeichner der Basisklasse zu- greifen, indem du den Zugriffs- operator verwendest. public: void printO const ( cout « "Co2 pro KWh : cout « "Cent pro KWh: Strom::print(); *1 > }; ” « getCo2KWh(> « endl; '» « geccc4KWh(} « endlj *1 Mithilfe der Zugriffs- operatoren t« kein Problem uir gleichnamige Elemente der B.i'.iUclaiw zuzugreifen Das Thema Zugriff auf das Erbe wurde zwar schon zum Teil angerissen und soll hier daher auch nicht ausufern. Aber hier soll nochmals die Elementfunküon Atomkraft:: print () herhalten. Dank einer Vererbung ist auf jeden Fall folgender Zugriff über Elementfunktionen auf die Daten möglich: II atomkraft.h dass Atomkraft : public Strom { private: unsigned int Co2KWh; Zbgaleitet« Clanen 813
*2 Dank der Vererbung kennen wir auch ohne Zugriffsoperator oder andere Umwege über die geerbten Elementfunktianen Strom::getQuellef) und Strom: : getKWhf) auf die Daten iugre»fcn O*e Ausgabe haben wir bisher mit Strom: :print() gemacht (was wir zuvor ja auch behandelt haben!. gecCo2KWh(> getct4KWhO getQuclleO gecKWhO « float ctAKWh; public: void printO const { cout cout cout cout J^in^^lemcn^^^Scr^er I Klasse Atomkraft, um auf CoZKWh - uj; :l c t,4KWh>^y^jgjg^ "Co 2 pro KWh "Cent pro KWh "Stromquelle "KWh « endl « endl « endl; *2 endl; JHinter^rundlnlol Dass du ohne Zugriffsoperator auf die Elementfunktionen der Basisklasse zugreifen kannst, liegt an der Art und Weise, wie der Compiler nach entsprechenden Bezeichnern sucht. Zunächst sieht der Compiler in der eigenen Klasse nach, ob ein Bezeichner mit entsprechenden Namen vorhanden ist. Falls nicht, geht der Com- piler in der Klassenhierarchle eine Klasse hoher und sucht in der Basisklasse nach dem Bezeichner. Handelt es sich hier auch nur um eine indirekte Basisklasse, geht der Compiler so lange eine Stufe nach oben, bis er einen passenden Bezeichner findet. Gibt's keinen passenden Bezeichner, meckert der Compiler und gibt auf. Bei gleichnamigen Bezeichnern erhält natürlich der Bezeichner als Erstes den Zuschlag, der nach der Klas- senhierarchie am nächsten steht. Problematischer wird es allerdings dann beim direkten Zugriff auf die Daten II atomkraft.h dass Atomkraft : public Strom { public: •1 O.i CoZKWh und ct4KWh private Mitglieder der KUssc Atomkraft s>nd. können wir natürlich hier in der Elementfunktion Atomkra ft::print() darauf mgieifei». 4 2 Hier geht aber dann nicht mehr quelle und KWh sind private Mitglieder der GasiskUsse Strom, und da wird jegliche« Zugriff verweigert > 1; void printO const { cout « "Co2 pro KWh : 11 « Co2KWh « endl; •1 cout « "Cent pro KWh: ft « ct^KWh « endl; •1 cout « "Stromquelle : 1t « quelle « endl; •2 cout « "KWh : ir « KWh « endl; *2 Ja, aber das wäre ja nicht im Sinne des Erfinders. Zum einen würdest du damit anfangen, das Klassendesign aufzuweichen, und zum anderen ist es in diesem Fall unnötig, weil es dafür ja die Elementfunktionen in der Klasse Strom gibt, welche dafür erstellt wurden. Trotzdem schön, dass du den letz ten Abschnitt mit den Zugriffs recht en so gut verstanden hast. quelle KWh /«n Strom protected private ^4/^ 384 Kapitel Cif
Unser Anlageberater verwaltet das Erbe Ich denke, der Umgang mit dem Erbe selbst sollte dir jetzt keine Kopfschmerzen mehr bereiten. Deinem überzeugenden Kopfnicken wollen wir natürlich nachgehen und ein paar Zeilen in der Praxis testen. t$chwicri(« Avfpbrk Jetzt wollen wir die Klasse Atomkraft um eine Element- funktion calc( > erweitern. Und zwar so. dass du anhand der vorhandenen Daten, wie der gesamten KWh. berechnen kannst, wie viel Gramm COj insgesamt für eine Instanz der Klasse Atomkraft in die Luft geblasen werden und wie viel € die Herstellung kostet- . / Okay, eine kleine Hilfe! Guck dir folgenden Codeausschnitt dazu an: a f Sa Atomkraft melier); meilerl .setQuelle("Atomkraft'’) melier),sctKWh(1000); melierl.setCo2KWh(16) meilerl.setct4KWh(2.65) Mithilfe dieser Otten kannst du den gesamten Co2-Aussto6 der Stwnmenge iKWh) berechnen iCo2KWh * KWh: und die Herstellungskosten für diese Strommenge (ctAKWh * KWhi If atomkraft.h dass Atomkraft : public Strom { public: void calci) const { cout « gctQuclleO cout « getCo2KWh() cout « getct4KWh() 1 « ” mit " « getKWhO « "KWh « * getKWhO « " Gramm Co2; * getKWhO / 100 « " Euro” « endl; Prima gelöst* Sinn und Zweck dieser Aufgabe war es natürlich, die Funktionalität deiner Klasse zu erweitern und zu überprüfen, ob du den Umgang mit den Daten der abgeleiteten Klasse und Basisklasse beherrschst. 385
Kode bearbeiten) Im Grunde würde ich dir empfehlen, wenn es möglich ist, dass du den Zugriff auf Daten einer Klasse (sei es nun die abgeleitete oder die Basisklasse) immer mit Elementfunktionen durchfuhrst. Bei den privaten Daten der Basisklasse ist ohnehin nichts anderes möglich. Das Programm bei der Ausführung: A O____ Atomkraft Datier I d « r $ , / a 1 ö m Co 2 pro KWh 2 C • ft pt e> KWh; 2,43 S I r o m q w • I I « Atomkraft KWh ; 1000 AI »ttk r o F I mi l I 0 0 0 K Wh - 16 0 0 0 Gramm C o 2 ; 26 5 Ewra D i « 1 « r I o • r $ IfiaUchf Aulgibrl Jetzt noch eine einfachere Aufgabe. Bei der Ausgabe von Atomkraft::print() läuft das Programm plötzlich Amok! Was wurde hier falsch gemacht? Ausgabe verliert die Kontrolle: dass Atomkraft : public Strom { publle: void printO const { cout « "Co2 pro KWh : cout « "Cent pro KWh: " « gecCo2KWh() « endl ” « getct<iKWh() « endl printO 5 } >5 Ganz genau! Wegen des fehlenden Zugriffsoperators auf die Basisklassenl-Iemen: Funktion ruf: sich die Elemcncfunküon Atomkraft t : print ( ) ständig neu in einer lüidlosrekursion auf, wodurch Sieh das Programm nicht mehr auf geregeltem Wege beenden lasst. 38» ELF
Ordentlich angelegt Nachdem du hier sauber milgemacht hast, kannst du eigentlich deinen Anlagebcrater feuern und dein eigenes Ding drehen. Fassen wir auch hier nochmals zusammen, was du über die Verwaltung und Erweiterung des Erbens erfahren hast: • * Deine direkte abgeleitete Klasse wird schon mal automatisch um die public und protected Mitglieder der Basisklassc erweitert. Bei gleichlautendem Bezeichner (sei es nun eine Elementfunktion oder seien es Daten) erhalt immer der Bezeichner den Vorzug, welcher der verwendeten Klasse in der Klassenhicrarchie am nächsten sieht. Du könntest auch sagen, das lokalste Mitglied wird verwendet. Solche gleichlautenden Bezeichner werden auch als Redefin ition bezeichnet, ** Willst du bei mehreren gleichnamigen Bezeichnern auf einen bestimmten überge ordneten Bezeichner zugreifen, musst du den Klassennamen, gefolgt vom ZugrilTs- operator, dafür verwenden (Klassennatne: : Bezeichner). * ** Der direkte Zugriff auf private Daten einer Basisklasse ist nicht möglich. Hierzu wird empfohlen, eine Elementfunktion in der Basisklasse zu verwenden, welche Zugriff auf die privaten Daten hat. OOP-typisch halt. Ein Aufwcichcn der privaten Daten in der Basisklasse mit dem Schlüsselwort protected kann ich dir nur bedingt empfehlen. tfiaUch» Auigj.be! Zum Schluss hier noch eine einfache Übung. Im folgenden I Listing ist der Redefinitionswahnsinn ausgebrochen. Kannst du sagen, was in weicher Reihenfolge auf dem Bildschirm aus- gegeben wird? Das Listing mit dem Redetinitionswahnsinn: dass Basisklasse ( public: void sagwasO { cout « "Bin der Bossen”5 } >; dass Subklasse : public Basisklasse { public: void sagwasO < Basisklasse: :sagwasO; } }; dass SubSubklasse : public Subklasse { tu»nr 397
public: void sagwasO < SubKlasse::sagwas(); ) >; dass SubSubSubKlasse : public SubSubKlasse { public: void $agwas() ( cout « *Ich bin der Depp hieran1*; ) int main() { SubSubSubKlasse sssKLasse; sssKlasse.sagwast); sssKLasse.SubKlasse::sagwas(); sssKlässe.SubSubSubKlasse;:sagwas(); sssKLasse.SubSubKlasse::sagwas(); Das Beispiel mit dem Durchrdchungs- und Wclterieitungs- Nirvana soll natürlich keine Schuir machen. Hier die Ausgabe des Programms (auf dem Kopf): Aufgabe d« Redrfiniliemwjhntinntbfiipk'h iVrloKnurtgaösvng) Hammerstark, jetzt bi$ du schon weit mit dem Thema Vererbung gekommen. 388 Elf
Ein superfettes Thema fehlt uns noch bei der klassischen Vererbung in C++. Und zwar die Konstruktoren und der Destruktor. Ohne die Dinger geht es nämlich nicht! Zuvor musst du natürlich noch wissen, wie beim Anlegen eines Objektes einer abgelei- teten Klasse die Konstruktoren aufgerufen werden. Der Aufbau eines Objektes erfolgt hier stets ausgehend von der Basisklasse von oben nach unten. Es wird also immer zunächst der Konstruktor der Basisklasse aufge rufen. Erst dann folgen die Konstruk- toren der abgeleiteten Klasse. Hier ein solches Beispiel: dass Opa { ... }; dass Papa : public Opa { ♦ ♦♦ dass Sohn : public Papa { ... }; Die Reihenfolge« in der die Konstruktoren aufgerufen werden, wenn du ein Objekt Sohn erzeugst: 1. der Konstruktor Opa 2. der Konstruktor Papa 3. der Konstruktor Sohn Der Aufruf des Konstruktors der jeweiligen Basisklasse muss daher immer hei der Definition des Konstruktors der abgeleiteten Klasse erfolgen. Die Bf«hcriM|cc beim Aufbau e«nci Otijektei dkf Klast* 5ö^n abgeleitet« ClMitn 39»
Beim Abbau von Objekten mit dem Destruktor ist dies genau umgekehrt. Hier wird der Destruktor der aktuell abgeleiteten Klasse ausgeführt bis hoch zur Basisklasse. Für den Fall, dass du einen eigenen Destruktor schreiben musst und das Objekt Sohn zerstört werden soll, lautet die Reihenfolge dann: >1. der Destruktor Sohn 5. der Destruktor Papa 6. der Destruktor Opa [HiftUujfundiflfö] Bei Destruktoren ist die Sache natürlich wesentlich einfacher als bei den Konstruktoren, weil hier keine Parameter benötigt werden und weil diese in der Praxis so gut wie nie direkt aufgerufen werden müssen. Daher musst du dir auch weniger Gedanken darum machen, welche Klasse hier von welcher abgelei- tet wurde. Die Syntax, um einen Konstruktor der Basisklasse in der abgeleiteten Klasse aufzurufen, sieht folgender- maßen aus: Abgeleitet::Abgeleitet(paraX, ...) : BasisfparaA, ...) < II ... > 0>c drr DrrtruHwn. wnn Objekt dk« KUmv Sohn 2<fiton werden -voll Wie du hier siehst, folgt immer nach den Klammem für die Parameter der abgeleiteten Klasse der Doppelpunkt, gefolgt vom Aufruf des Konstruktors der Basisklasse. Hierbei kannst du auch gleich die Parameter an den Konstruktor der Basisklasse übergeben. 390 Elf
Natürlich kannst du mithilfe des Elementinitialisierers auch die einzelnen Daten der abgeleiteten Klasse, getrennt von einem Komma, wie gewohnt zuweisen. Trotzdem muss hier immer an erster Stelle hinter dem Doppelpunkt der Konstruktor der Basisklasse stehen! Andernfalls dürfte sich der Compiler bei dir melden. Du kannst ein Objekt einer abgeleiteten Klasse auch einer Basisklasse zuweisen, In dem Hill erfolgt eine interne Umwandlung] Wobei nur die Daten der Basisklassc von der abgeleiteten Klasse dem Objekt der Basisklasse zugewiesen werden. ----------------------- Sohn sohnOl; Opa opaOl - sohnOl; *1 Eine umgekehrte Zuweisung von der Basisklasse zu einer abgeleiteten Klasse funktioniert allerdings so nicht mehr, weil die Daten an dir abgeleitete Klasse nicht zugeordnet werden können. *1 Dies ist eine implizite Typ- utn Wandlung van de- Kl.iv.r Sohn ru<ti Opa Opa bekommt n.iltirlich nur die Daten zugewiesen, die er selbst enthalt Ole anderen Daten aus der Klasse Sohn bzw Papa werrfers hier nicht beachtet Ibgiltitm 3H
Wer bekommt was In der Theorie hören sich diese Konstruktoren bei der Vcr- erbungssache schlimmer an, als sie oft sind. Daher wollen wir uns kurz ansehen, wie wir das mit der Atomkraft und Strom Klasse handhaben. In unserer abgeleiteten Klasse Atomkraft haben wird den (inline) Konstruktor der Basisklasse Strom aufgerufen, wie folgt: II atomkraft.h «A4 Atomkräfte unsigned int co2«0, float et»0-0* string q“"”> unsigned int k»0> : Stromfq» k)» Co2KWh(co2)» cc4KWh(cc) {} *1 ’1 Hier sichst du, *.<? gleich hinter dem Doppelpunkt der Konsuukiüf der Basisklasse Strom aufgerufen wird. Hier wurden auch gleich, getrennt von einem Komma, die einzelnen Werte de« Abgeleiteten Klasse Atomkraf t Wl per Elementinitialnierer initialisier!. Ist dir das mit dem Elementinitialisieren zu komplex, kannst du natürlich auch Folgendes schreiben: Atomkraft(unsigned int co2"0> float ct-0,0> string Q“”*1» unsigned inc k"0) j Strom(q, k) *1 setct4KWh(ct); *2 setCo2KWh(co2); ’2 } __________________________ ___________________ “2 Aber die pnvaten Daten der eigenen Klasse kannst do natürlich auch über die entsprechenden Elementhinktionen inrtkiliueren. •1 Für die Daten der Basiskiassc kommst du auch so n>cht um den Konstruktor via Elementinitialrsiecer herum. iZettril Oie Verwendung der extra Aufrufe über die set-Elementfunkionen ist natürlich die schlechteste Alternative, weil du so zum einen in Kauf nimmst, dass manche Daten doppelt geschrieben werden (nämlich einmal mit dem Standard-Konstruktor und einmal mit den set-Elementfunktionen). Ich wollte es hier allerdings nur demon- strieren. weil man doch gerne mal weitere Arbeiten in den set-Sachen wie das Überprüfen von Werten usw. vornimmt, 392 cif
|(ir»f«hc Auffab*] Was passiert bet dem folgenden Codeausschnitt, und was wird hier mit print () auf dem Bildschirm ausgegeben? Der besagte Codeausschnitt: Atomkraft meilerl; meilerl.sctQuelle("Atomkraft AWE"); meilerl.setKWht10000); meilerl.setCo2KWh(22); meilerl♦setct4KWh(2,65); Strom stroml meilerl; *1 stroml.print(); ‘2 Die Losung: ”1 pwiert Wer? ’2 Wh wird hier autgc&rbrr»? Mcf Atomkraft fh </&$. Strom tr Völlig richtig! |Erled<i^t| Super mitgemacht! Jetzt sind wir bald durch mit dem Thema, und du kannst dich als erfahrener Anwender von Vererbungen bezeichnen. Clam-n 383
Du kannst gerne in deiner meditierenden Position bleiben, weil du zunächst eh nur ein paar Fragen beantworten sollst. Also immer schön „Uuuhmmm" sagen. [(nfah* Awfg-lta) In welcher Reihenfolge werden beim Anlegen von Objekten einer abgeleiteten Klasse die Konstruktoren aufgerufen? Und andersherum, in welcher Reihenfolge werden beim Zerstören die Destruktoren aufgerufen? i. Der Aufbau von Objekten einer Basisklasse erfolgt von oben nach unten. Zunächst wird also der Konstruktor der Basisklasse aufgerufen. Erst dann folgen der oder die Konstruktorten) der abgeleiteten Klassefn). 2. Die Destruktoren hingegen werden in umgekehrter Reihenfolge aufgerufen und abgebaut. Hierbei wird zunächst immer der Destruk- tor des aktuell verwendeten Objektes der abgeleiteten Klasse benutzt bis hoch zur Basisklasse. Prima, das shzt Jetzt, glaube ich! JEmüche Aulgibel Kannst du eine abgeleitete Klasse an eine Basisklasse zuweisen und umgekehrt? *2-1-.-. £ 4ZO->-J C^-i Jawohl! Auch das Ist richtig! {Schwierige Aufgibek Zum Schluss noch eine Aufgabe, um zu sehen, ob du alles verstanden hast. Kannst du mir sagen (ohne das Programm auszuführen), welche Ausgabe der folgende Quellcode erzeugt? 384 c*o;t«l Cif
Der Codeausschnitt: dass Opa { private: int wert_O; public: Opa(int val*0) : wert_O(val) O void printO < cout « wert_O « endl; } >; dass Papa : public Opa { private: int wert_P; public: Papa(int val**0) : Opa(val+val) { wert_P val; > void printO < cout « wert_P « endl; } dass Sohn : public Papa { private: int wert_S; public: Sohn(int val-0) : Papa(val+val) < wert_S = val; } void printO < cout « wert_S « endl; } >; int mainO < Sohn sohnOiOOO); sohnOl.printO; *1 sohnOl.Papa::print(); sohnOl .Opa::printO ; Opa opaOl; opaOl .printO; opaOl • sohnOl; opaOl .printO; "t*~ |•elQhflu^£/L6»u«l(| Ein Thema noch4 und du hast die Ver- erbung durch! Tank nochmals frische Energie für die letzte Hürde. Indem du tust, was dir Spaß macht. Spiel meinet- wegen eine Runde WoW oder zieh dir ein paar DVDs rein. M Was wird hier dusgcgeben? 385
Mehrfachvererbung Neben einer einfachen Vererbung gibt es in C++ auch die Möglichkeit einer Mehrfachvererbung. Das bedeutet, du kannst auch eine neue Klasse von mehreren Basisklassen ableiten. Solch eine Vererbung entspricht dann schon eher unserer Natur: Hm, mit dem Kind k£rirt|f rruri La<1 Mitleid hibtn, Vorfahren be»Sem ibiwsUmmrn. Hierbei werden, wie auch schon bei der gewöhnlichen 1:1-Vererbung, alle entsprechenden Daten und Eie- mencfunkiionen mit denselben Regeln weite rvererbt, Eine Mehrfachvererbung kannst du relativ einfach schreiben. Nehmen wir mal an, du hast neben der Klasse Atomkraft noch eine fast gleiche Klasse Windkraft. Jetzt willst du die Funktionen und Daten der beiden Klassen in einer Klasse Stromanbieter verwenden. 396 uoit«i cif
Folgendermaßen kannst du die Ableitung durchführen: dass Stromanbieter : public Atomkraft, public Windkraft { *1 H ... >» “1 Hinter dem Doppelpunkt niuiit du lediglich, gevennt von einem Kommj, die Ktaten ifigeben, von denen geerbt wird. Für jede Ktate musxt du Außerdem die ZugnfiSrrchtc public. protected oder private hinzufügen (Achtung] Solltest du vergessen, die Zugriffsrechte für eine Klasse zu schreiben, wird automatisch private verwendet! So kannst du dir die Mehrfachvererbung vorstellen: MthftiehvwtrtJuftg fvr fiAe« ordentlichen Strom-Mia Abgeleitete Minen 387
Mehrfachvererbung in der Praxis *3 De* Konstruktor ist natürlich etwas vollgestopfter, um die ßasisktasen mit Daten zu versorgen. Dies hier sind die Daten für den Konstruktoi Atom- kraft Sachte, sachte. Schrödinger. Wollte gerade ein Beispiel schreiben. In dem Beispiel wird davon ausgegangen, dass wir die Klasse Windkraft auch bereits geschrieben haben. Die Klasse Windkraft ist hier exakt so aufgebaui (inklusive einer Ableitung von der Basisklasse Strom) wie die Klasse Atomkraft. Selbst die Daten und Elementfunktionen sind dieselben. Einzig der Konstruktor- name ist logischerweise anders. |(i«fichr Au<K4b<l Wenn du Lust hast, kannst du ja die Klasse Windkraft selber schreiben. Und wenn nicht, dann musst du halt die DVD des Buches einlegen. Von diesen beiden Klassen Atomkraft und Windkraft wollen wir jetzt die Klasse Stromanbieter ableiten 398 Cacut«! Cif i Hier le ten w>r die Klasse von Stromanbieter Stromanbictcr v<xi den Klassen Atomkraft und Windkraft .m D*e Klassen Atomkraft und Windkraft vnd somit d- Basisklassen <iostrcacn> <strlng> "atomkraft.h" ,twindkrafc.hti // Stromanbieter-h linclude linclude linclude linclude using namespace std; lifndef STROMAN Bl ETER_H Ideflne STROMANBIETER K dass Stromanbictcr:public Atomkraft Windkraft { *1 private: string Anbieter; *2 “2 Der £ n^ thtieit halber wollen b*ri’ liier die Klasse n cht n-|f allzu Vielen Daten überfrachten, ub
public: StromanbleterCunsigned int co2»0» float ct*O»Or *3 string q-,,M> unsigncd int k«0t *3 unsigned int co2_2-0, float ct_2-0*0> *4 string q_2*’r"f unsigned int k_2Ä0, ’4 string a«****) : Atomkraft(co2, ctT qr k) , *5 Windkraft(co2_2t Ct_2t q_2, k_2) "6 *5 Ot • Konstruktor der Ba^sMassc Atomkraft wird mit Werten autgeruflen. '6 dasselbe nochmals mit dem Konstruktor Windkraft anbieter = a; *7 } void setAnbieter(const string a) { "8 anbieter = a; } string getAnbiecer() const { *8 return anbieter; } >; #cndif ’? 0 <• Daten der abgeleiteten KIjul Stromanbieter wrrdrn mit Werten liWiil In diesem Abschnitt blenden wir bewusst nochmals die oberste Basisklasse Strom aus. {Achtung/Versieh!) von der ja die Klassen Atomkraft und Windkraft abgeleitet lind. Spätestens, wenn du die Klasse Stromanbieter verwenden willst, dürftest du bemerken, dass die Sache komplizierter ist. als zunächst angenommen. Schließlich sind in den Klassen Atom- kraft und Windkraft Elementfunktionen mit denselben Namen und demselben Sinn vorhanden. Zudem kommt jetzt ja noch dazu, dass Atomkraft und Windkraft eine Basisklasse Strom haben {jede eine eigene). Ja, jetzt können wir die Basisklasse Strom nicht mehr ausblenden. Bei ßasisklassenrmtgliedern mit dem gleichen Namen musst du wieder den Klassen- namen und Bereichsoperator verwenden {Klasse: :Hitglied), damit der Compiler solche Mitglieder auflosen kann, Hier die zunächst etwas unbequcrni' Verwendung unserer Klasse Stromanbieter: linclude "atomkraft.h" linclude "windkraft.h" linclude "stromanbieter.h” linclude "ström.h" 389
using namespace std; •3 Und hier bekommt die Wrndkrateeite ihre Werte. •1 Miet legen w<r ein neues Objekt Stromanbieter an ’2 An dieser Stelle wessen wir der Atomkraftseite Ihre Werte zu. int mainO { Stromanbieter anbieterOl; *1---— anbieterOl.Atomkraft::setQuelle("Atomkraft”>; anbieterOl.Atomkraft:;$etKWh(I2000)i V anbieterOl.Atomkraft::setCo2KWh(22); anbieterOl.Atomkraft::sctctAKWh(2.65); anbieterOl.Windkraft::setQuelle("Windkraft"); anbieterOl.Windkraft;:sctKWh(12000) anbieterOl.Windkraft::setCo2KWh(10) anbieterOl.Windkraft anbieterOl .Atomkraft: :calc() anbieterOl.Windkraft::Cale() Das Programm bei der Ausführung Schwere Sache, so ein SLram-Mij'. Wmdhfill it iwj» teurer jh Al<>fnkfAllr macht aber deutlich weniger Dmfr "4 Am Ende wollen wir noch zum Vergleich ^utgeben, was für eine StigmhcnJellung mehr Dreck macht und wekhe die billigere Variante ist. An dieser stelle muss kh anmerken, dass die von mir verwendeten Werte reine Durthschnim- werte sind. Du werfil p, die Welt dreht sich Khncll. Zu diesem Beispiel muss ich natürlich hinzu fügen, dass ich mit Atomkraft und Windkraft mit Absicht Basisklassen verwendet haben, welche identische Namen für die Elementfunktionen haben. In der Praxis wird es wohl eher sehen der Fall sein, dass es in zwei Basisklassen identische Bezeichner gibt. Ich wollte dir hier nur vorfüh- ren, dass die Verwendung von Mehrfachvererbung die Sache durchaus komplexer machen kann. 400 c*o‘.t«l cif
Lohnt sich die Mehrfachvererbung überhaupt? An dieser Stelle angekommen. stellt sich für dich sicherlich die eine Frage, nicht wahr? Du fragst dich jetzt sicherlich...! Hey, Schrödinger, die Frage! Das ist ein wirklich gute Frage, die du da stellst! Gerade wegen solcher Namenskonflikte, wie du sie in der Praxis sehen konntest, wird der Code ziemlich verkompliziert. Wenn du eigene Klassen schreibst, hast du das natürlich selbst In der Hand und kannst deinen Code so schreiben, dass du eventuell auf Mehrfachvererbung verzichten kannst. Wenn du allerdings keinen Einfluss auf die Basisklassen hast, kommst du manchmal einfach nicht um die Mehrfach vererbung herum. Aber du weißt ja jetzt, worauf es dabei ankommt, und vor allem, worauf du dabei achten musst. (Ei*u«h* AutgAb*] Erweitere deine abgeleitete Klasse Stromanbieter jetzt um eine Elementfunktion printO, mit der alle Daten der Basis- klassen ausgegeben werden. Wenn du umsichtig vorgehst, brauchst du hierfür nicht viele Zeilen Code zu verschwenden. Hierzu meine Musterlösung: II Stromanbieter.h dass Stromanbieter : public Atomkraft, public Windkraft { public: . . _ *1 DHinilidn der El^mentfunktiön void printO const (1 » -r - » — — Stromanbieter: :printO 401
Atomkraft::print() Windkraft::print() > >; 2 Ml 3 « *2 Alte Daten, vom Atomkraft-Teil bt5 hin zur rcddinicftcn Elcfncntfunktion Atomkraft::print<). werden ausgegeben. *3 Und hier werden alle Daten, vom Windkraft-Tri1 M h fi Ulf Elcmcfltfunktion Windkraft::print() -iZCh/ AöCJ icl^ tM/Prf, f’/ass Atomkraft Strom 4x6 Strom Du bist hier Jetzt ein bisschen voraus. In der Tat kann man da was machen, aber das erfährst du auf der nächsten Seile. in///"-, (•tlohnunxilojungl Das mit der Mehrfach Vererbung hast du wohl verstanden. War ja auch nicht so schwer. Bevor du weitermachst, empfehle ich dir wieder, etwas für dich selbst zu tun, um den Kopf wieder freizukriegen. Wie wäre es mal wieder mit einem romantischen Abend mit deiner Freundin, die du in letzter Zeit etwas vernachlässigt hast? 402 ÜJCktel ELF
Oftmals willst du bei der Mehrfachvererbung eventuell vorhandene Basisklassen nicht zweifach im Speicher haben. Bezogen auf unseren Stromanbieter, den du im Kapitel zuvor geschrieben hast, sah der Stammbaum der Mehrfachvererbung ja wie folgt aus: Che oberste Basis- klar Strom ti hier zweifach Vorhände*. Zugegeben, in unserem Beispiel ist diese An der Mehrfach Vererbung gar keine so schlechte .Alternative, weil wir dadurch jederzeit die EnergieherslL'llung von Atomkraft und Wind- kraft einzeln behandeln und berechnen können. Willst du aber nur eine Basisklasse Strom haben, worauf dann Atom- kraft und Windkraft zugreifen würden, musst du eine virtuelle Vererbung daraus machen. 0\tt ? 403
Nein ganz und gar nicht. Im Grunde musst du jetzt nur ein lausiges Wörtchen, genauer das Wort virtual, in den Klassen Atomkraft und Wind- kraf t bei der Ableitung reinschrei- ben, und schon gibt es nur noch eine vir- tuelle Basisklasse Strom. Ware schön, wenn die Encrgiemultis da auch so trans- parent vorgehen würden! Also, nur das magische Wortchen virtual davorsetzen und verwenden, wie folgt: H Atomkraft,h dass Atomkraft >! // windkraft.h dass Windkraft >; virtual public Strom ( *1 virtual public Strom { *1 ' 1 das magische Wörtchen Da du jetzt Strom zur virtuellen Basisklasse von Atomkraft und Windkraft erklärt hast, sieht der Vererbungs- baum nun aus, wie folgt: Dinh det VirluälitAt gibt es- nm n«b «ine Batoklavkr St TODA wenn du ein Objekt der Klaoe Stromanbieter anlc£>' 404 [cf
Virtuelle Teilchen verwenden Mit einer virtuellen Vererbung hat ein Objekt der Klasse Stromanbieter somit nur noch diese eine virtuelle Basisklasse Strom Daher kannst du jetzt auch auf die öffentlichen Mitglieder der Klasse ohne den Bereichsoperator zugreifen, weil die geerb- ten Mitglieder der Klasse Strom jetzt auch nur noch einmal im Speicher vorhanden sind und somit eindeutig zugeordnet werden können. (Notiz] Dir sollte natürlich klar sein, dass eine solche virtuelle Klasse erst bei der Mehrfachvererbung Sinn macht Würdest du zum Beispiel von der Klasse Atomkraft oder Windkraft ein Objekt erzeugen, so hätte natürlich jeweils jede Instanz eine eigene Basisklasse Strom Wenn du nachträglich eine virtuelle Basisklasse deklarierst, musst du häufig noch den Konstruktor der abgeleiteten Klasse anpassen. Im Beispiel sind Ja Jetzt die Daten Strom: : quelle und Strom: :KWh nur noch einmal vorhanden. Hierzu ein Codeausschnitt, wie du auf die virtuelle Basisklasse Strom mit einer Instanz der Klasse Stromanbieter zugreifen kannst: Stromanbieter anbieterOl; 1 Oer Zugrili auf die Erement- anbieterOl .Atomkraft: :setCo2kWh(22) ; edolgt bei gleichnamigen Bezeichn anbieterOl .Atomkraft: :setct4KWh(2.65); *1 natürlich rieh wie vor über anbieterOl .Windkraft:: setCo2KWh( 10); '1 anbieterOl .Windkraft: :setct4KWh(3.5); "1 d#p Zugritfwpef.ilorcn. anbieterOl ♦setQuelleC'Atomkraft und Windkraft"); ‘2 *2 DerZuxriif Jirfdie anbieterOl.setKWhf 12000); *2 / / z/ üifentHclien Element- fu nkfkwie«! der vwtueÄen Strom|<t/t keinen ZifgriHMipe.’iRur mehr anbieterOl.print(); '3 4 3 Hier wird riitijilieh die Elementfunktiön Stromanbieter::print() airfgerufen! Strom Ja, damit hast du recht ... ICode benbeiten] • '•-LJ' Wenn du dir das Beispiel allerdings etwas genauer betrachtest, dürfte dir auffallen, dass es hier ein kleines Problem gibt, die Elementfunktionen calcQ von Atomkraft und Windkraft aufzurufen. Zwar kannst du die Elementfunktion calc< ) beider Klassen aufrufen, aber hier fehlt jetzt leider die eine genauere Angabe, die besagt, wie viel KWh die eine und wie viel die andere Energiequelle zur Verfügung stellt, weil es jetzt nur noch ein Strom: tKWh gibt. Daher fällt es jetzt schwer, eine exakte Berechnung des CO.-Austoßes und der Kosten durchzuführen. In dem Fall wäre es eine Überlegung wert, die private Variable Strom: :KWh statt in Strom jeweils In den Klassen Atomkraft und Windkraft zu implementieren, 406
Der Sinn und Zweck der virtuellen Ver- erbung dürfte dir wohl jetzt klar gewor- den sein? Kundeftfeuwarc Zwischen Virtualität und Realität Genau! Wenn du eine Klasse von mehreren Basisklassen ableitest, welche ebenfalls eine gemeinsame Basisklasse haben, kannst du mithilfe des Schlüsselwortes virtual durch das virtuelle Ableiten einer solchen Basisklasse verhindern, dass die Mitglieder der oberen Basisklasse mehrfach vererbt werden und somit nur noch einmal irn Speicher vorhanden sind. *0 Qq v . 0- Kreirth4ien4fer drottta« rutter |Schwi«fige Aufgabe) Sieh dir folgende Abbildung an. Versuch eine Klassen Hierarchie zu erstellen, wie du sie dort sehen kannst Mithilfe der Mehrfachvererbung und des Schlüssel- wortes virtual kannst du das realisieren Natürlich brauchst du jetzt hierzu keine weiteren Mitglieder in den Klassen zu verwenden. Es reicht aus, wenn du einfach nur Klassen mit einem leeren {) definierst und die abgebildete Hierarchie programmtechnisch umsetzt. Mach einen Code daraus 408 cir
Hier mein möglicher Lösungsansatz: dass Firma O; "1 "1 die oberste BzsitkUtfe Firma dass Kleinhaendler : virtual public Firma {}; "2 dass Grosshacndler : virtual public Firma {}; *2 *2 Davon kltcn wir ehe beden KUsseo Kleinhaendler und Gross- ha end ler ab Damil beide Klassen nur •_ ne Klasse* Firma ab BasisJdasse haben, haben wird die Vererbung virtuell umgesetrt. Bis jetzt nüLrt allerdings die virtuelle Vererbung noch nichts. dass KundeNeuware public Kleinhaendler, public Grosshaendler {}; *3 dass Auktionshaus : virtual public KundeNeuware {}; *4 dass SecondHandShop : virtual public KundeNeuware {}; “4 dass KundeGebrauchtWare : public Auktionshaus» public SecondHandShop {}; B5 *4 Die Kime KundeNeuware wird jetzt ebenfalls zu einer 8asjr$ktj$$e für d»e abgeleiteten Klassen Auktionshaus und SecondHandShop Damit die beiden Klassen auch bei einer mehrfachen Vererbung später nur eine Basisklasse KundeNeuware enthalten, haben wir das Ganze natürlich wieder virtuell gemacht. *5 Am Ende schließen wir den Kreis wieder indem wir die Klasse KundeGebrauchtWare von den Klassen Auktionshaus und SecondHandShop ^Weiten Dank dieser Mehrfachvecerbung hat die Klasse KundeGebrauchtWare die beiden Klassen Auktionshaus und SeconHandShop als Basiiklasvc Die Klasse KundeGebrauchtWare hingegen ist dank einer virtuellen Vererbung wieder nur einmal im Speicher vorhanden. Natürlich wurde unserer Klasse auch die oberen ßasisk lasse»' Kleinhaendler und Grosshaendler vererbt Und die alleroberste Basisklasse Firma ist natürlich auch hier wieder nur einmalig im Speicher vorhanden. ‘3 Bei der Klasse KundeNeuware führen w.r jetzt eine Mehdachvererbung durch. Diese Klasse hat jetzt mrt Kleinhaendler und Gross- haendler zwei ßas^klassen aber nur eine oberste ßawklasse Fi rma (wegen des Schlüsselwortes virtual im Schritt zuvor). Itelohnungrioivn^) Prima, Schrödinger! 407
Sicherlich stellst du dir manchmal die Frage, wie Programmierer anfangen können, ein Programm zu schreiben, von dem Sic vielleicht im Augenblick noch nicht einmal genau wissen, wie sie es in der Praxis realisieren können. Okay, ein Beispiel: Stell dir mal vor, du willst einen Musikplay- er schreiben. Jetzt schreibst du eine Elementfunktion für die Wiedergabe. Mal ehrlich, kennst du dich mit den vielen Audio- formaten wie MP3, OÜG. WMA, WAV usw. aus? [Hir»lrrjrpnd«nFo] Du wirst sicherlich nicht hergehen und bspw. das MP3-Format selbst encodieren. Hierfür verwendest du in der Regel eine fertige Bibliothek, wie zum Beispiel LAME MP3 Encoder (siehe hierzu auch http://lame. tourcejorge.net/}. Andere Frameworks bieten dir hierfür vielleicht auch andere fertige Bibliotheken an, um bestimmte Audio- formate in dein Programm einzubinden. Damit du dich jetzt nicht selbst bzw. sofort um die Implementierung der einzelnen Audioformate kümmern musst, erstellst du lediglich eine Klasse, die nur die Schnitt* stelle fÖr weitere Datenformate vorgibt. Eine solche Klasse wird auch als abstrakte Klasse bezeichnet. Damit eine abstrakte Klasse auch abstrakt ist, muss mindestens eine abstrakte Elemenlfunktion enthalten sein. Allerdings darfst du eine solche Elementfiinktion nicht in der Funktion implementieren. Es darf ledig- lich der Elementfunktionskopf enthalten sein. Mithilfe dieser Signatur legst du fest, welche Parameter diese Elemenlfunktion erhält und was für Werte zurückgegeben werden. Und damit auch dein Compiler Bescheid kriegt, dass du hier nicht die Definition vergessen hast, sondern was Abstraktes machen willst, musst du das bereits bekannte Schlüsselwort virtual verwenden. 408 cif
Hierzu ein Codeausschnitt, der dir eine solche abstrakte Klasse demonstrieren soll: II Audioformat.h dass Audioformat { publict virtual void abspielenO = 0; *1 virtual void info() 0; *1 ’l Dicm- Elementfunktionen wurden mit dem Schluwlwrt virtual ab virtuell grtcnweichnrt. Damit inan diese Mitgliede • nicht noch aus Versehen aijlrulefl kann oder gar Insunzen von der Klasse erzeugt, wurden diese noch extra md 0 initialisiert. Damit st es unmöglich rin Objekt der KUsve Audio formst zu erzeugen virtual markiert eine tlemendunki»o<i ab ,vmuetl" und das -»CT macht sie zu einer rein virtuellen (pure v>nuai) ElerneM- funktion. O^jt^ Da hast du natürlich recht! Eine abstrakte Klasse allein macht noch keinen Sommer und nützt niemanden was. Diese Klassen bekommen erst ihren Sinn in Zusammenhang mit einer Vererbung. iHintefg r undinfal Wenn du eine Klasse yon dieser abstrakten Klasse ableitest, vererbst du Ja auch die virtuellen Elementfunktionen. Daher musst du die in •• i> '> :cn Klasse virtuellen Elementfunktionen (also die, die mit .»0‘ markiert wurden.} überschreiben und implementieren Andernfalls würde aus der abgeleiteten Klasse wiederum nur eine weitere abstrakte Klasse. Mit der abstrakten Basisklassc zwingst du praktisch der davon abgeleiteten Klasse eine Schnittstelle auf, welche geschrieben werden muss. Jn unserem Fall müssen also in der abgeleiteten Klasse von Audioformat die beiden Elementfunktionen abspielen( ) und info( ) Implementiert werden. 409
Abstrakte Vielgestaltigkeit Damit dir diese abstrakte Klasse nicht zu abstrakt wird und du den Sinn und Zweck solcher Klassen auch verstehst, wollen wir zur Praxis schreiten. Uni also in unserem Beispiel mit der abstrakten Basisklassc Audiofonnat Dateien im MP3- oder OGG Format abspielen zu können, erstellen wir hier jeweils eine Klas- se MP3_Format und Ogg_Format. welche wir von der Klasse Audiofonnat ableiten. Aufgrund der Ableitung von der abstrakten Basisklasse Audio- format sind wir gezwungen, die beiden als virtual gekennzeichneten Elementfunktioneii abspielen() und info<) zu implementieren. Und so läuft dies (natürlich vereinfacht und alles als inline) in der Praxis ab: (I MP3.h linclude "Audiofonnat.h’' *1 1 Oer He.ider mutt MturlKh mit rc»n *2 Hrtt wird die Ableitung von der virtuellen BasiskUss« Audiofonnat durrhjje führt. dass MP3_Fortnat : private: string song; public: MP3_Format(const void abspiclenO cout « song « " > void InfoO { *4 » - ----- cout « "Der Song ist eine MP3-Datei\n”: public Audioforoat { *2 4 3 nieht ohne meinen Komtriiktor stringi s) ; song(s) {} TU < M .—!------- — wird gerade gespielt\n"$ MP3_Format alles fi> f-ri'n.'flrtufilrttonr-i I und üaten wiwendcst, diese teilten Element- Fiktionen müssen aut leiljjfl Fall geschritten •bvrrtjen, sonst vrrweiger1 dir der Compiler die (Code beaibtiten) Die eigentliche Funktion für abspielen() und info() besteht natürlich nur aus einer Dummy-Testausgabe! Statt einer Dummy-Ausgabe müsstest du jetzt hier echte MP3-Funktionen (wie bspw. den LAME MP3-Encoder) in deinem Programm ein- bauen. So können du und andere Programmierer jederzeit Unterstüt- zungen für weitere Audioformate nachimplementieren. 410 ei?
Soll es bspw» das Ogg-Format sein (natürlich auch wieder nur bestehend aus einer Dummy-Ausgabe): ff ogg.h linclude "Audioformat.h" • 4 dass Ogg_Format : public Audioformat { private: string song; public: Ogg_Format(const scringi s) : song(s) {} void abspielent) ( cout « song « " wird gerade gespielten”; > void info() { cout « "Der Song ist eine Ogg-Datei\n”; > ); Jetzt wird es vielseitig - Polymorphie Okay, die Erklärung dazu kommt sofort. Du hast ja in den beiden Klassen MP3_ Format und Ogg_Format jeweils die beiden F.lemcrufunkUoncn abspie- len ( ) und info( ) definiert. Diese Elementfunktionen ersetzen jetzt die leeren virtuellen Elemeniliinktloncn der Basisklasse Audioformat mit einer besonderen Version, welche logischcrweise vom Typ des Audioformates (MP3 oder Ogg) abhängt. Dank einer solchen Implementierung kannst du dich jetzt darauf verlassen, dass schon das richtige abspielen ( ) oder info( ) verwendet wird. Und ein solches Ver- halten wird eben als Polymorphie (= Vielgestaltigkeit) bezeichnet. Jetzt fehlt dir natürlich noch das Know-how, wie du diese Polymorphie in einem Programm verwenden kannst: 411
linclude "ogg.h" *1 ’ Die h<?oder brauchen wir natürlich auch hier. void abspielen(Avdioformat& fmt) ( ‘2 fmt.abspielen(); *2 > ’2 --------'— • Hier haben wir zwei glotw le Funktionen (I) abspiclcnf) info( ) mit dem Parann- Audiofortnat al void info(Audiofortnat& i) { *2 i.infoQ; *2 > *2 int malnO < MP3_Format songlCtest^ropJ”) abspielen(songl); *3 —— info(songl); *3 Ogg_Format $ong2("test,ogg") abspielen(song2); info(song2); ’3 return Oi > ' J Beim Funktionsaufruf musst du dich ict-jt um nichts mdir körn* mrrn. Dank def Polymorphie wird sdhon dir passende Elemcntfunktlün faf ein MP3_Format- oder Ogg Format - Objekt verwendet (Fehl».] Es ist wirklich von Bedeutung, dass du auf die Basisklasse über einen Zeiger oder eine Referenz zugreifsl. Warum? Wurde eigentlich bereits erwähnt. Weil du ja aus einer abstrakten Basisklasse kein Objekt erstellen kannst. Mithilfe von Zeigern oder Referenzen erstellst du somit auch keine Objekte und kannst mit den .echten* Objekten arbeiten, deren Adresse du als Parameter übergeben hast. Das bei Programm der Ausführung: 412 c«p;t«t Elf
Virtuelle Zerstörungskraft Falls du ein dynamisches Objekt der Klassen mit mindestens einer virtuellen Llemenlfunktion und im Ende deinen Abfall mit delete wieder entsorgst, kommt es zu einer bösen Über- raschung. Der Compiler erkennt nümllch bei der Freigabe nur den Typ der Zcigcrvariablcn. Ein Beispiel dazu: Audioforinac. *song - new MP3_Forrnat ("test-mpl"); abspielen(*song); delete song; *1 *1 Der Typ des Zeigen ist hier Audioformat Oah,.> ,:i&- delete tjiuchlith nur den Te>l des Audiofonnat Speicherbereiches frei. Auch wenn es vielleicht nicht gleich deutlich wird, haben wir ohne weitere Wor- kehnmgen ein Speicher!eck erzeugt, weil das Teilobjekt MP3_Format nach wie vor auf der Speicherhalde liegt und nicht mehr wer J *'- - - J-11 Um eine solche Verschmutzung des Speichert abstrakten Klasse auch den Destruktor mit v in der So muss das in deinem Fall dann aus. H Audiofonnat .h dass Audiofort&at { public: 1 Wenn du jetzt dynamisch einen Sperftier Jnfordent und zur Laufzeit fre*£ibM, wird dank des S<tiluwlworte\ virtual virtual virtual cout « "Destruktor > void absplelenf) = 0; void infoQ 0; -AudioformatC) { *1 virtual vqr dem Dertryktof jucti der Destruktor drr abgeleiteten Klaron ausgefährt IflMIgUI Hat deine Klasse mindestens eine virtuelle Elementfunktion, solltest du immer gleich auch einen virtuellen Destruktor dazuschreiben. Auch wenn du vielleicht keinen Destruktor benötigst. Man kann ja nie wissen! IbqilHtttl (Imin 413
Was vom Abstrakten übrig blieb Richtig eingesetzt, gibst du mit den abstrakten Klassen eine Schnittstelle vor. In unserem Fall kann somit Jederzeit ein anderer Programmierer irgendwo auf der Well weitere Audioformate hinzufugen. Ähnlich ist dies auch mit Programmen, in denen du Grafikformate verwendest. Auch hier wirst du selbst vielleicht JPEG und PNG hinzufügen. Allerdings gibt es noch eine Menge Formate mehr. Mit einer abstrakten Basisklasse kannst du es jetzt deiner Nachwelt überlassen, ob diese noch weitere Formate zu deinem Programm hinzufügt oder nicht. Auch Hard warehersteiler könnten so vorge- hen, indem sic mit einer abstrakten Klasse vorgeben, welche Funktionen der Treiber haben muss. In der Praxis ist diese Klientel allerdings eher selten so offenherzig. Ein weiterer Vorteil ist auch, dass du Ja gar nicht 100%ig sagen kannst, welches Format der Anwender eigentlich öffnet. Dank der Polymorphie kümmert sich dann dein Programm selbst darum, welche Element funktion aufgerufen wer- den soll. (Kirnfjchr Auigab«! Der folgende Codeausschnitt lässt sich nicht übersetzen. Was wurde da falsch gemacht? 414 ei*
Wo liegt der Fehler begraben? Sehr gut au (gepasst! int mainO { Audioformat *song new MP3_Forinat("tcst .mpl" ) ; abspielen(*song); Audioformat newsongCtest.ogg"); info(newsong); delete song; return 0; } A_* Was bist du denn? Wenn du auf der Suche nach einer Möglichkeit bist, den Typ deines Objektes zur Laufzeit zu ermitteln, kannst du typeid aus der Headerdatei <typeinfo> verwenden. Das kann besonders nützlich zum Ermitteln oder Vergleichen von poly- morphen Objekten sein. Hier ein Beispiel, wie du dies für deine Zwecke verwenden kannst: linclude <typeinfo> "1 Den Header WUch<*n A-tr r.4fijrt»Chauen <UW void TypetAudioformat& typ) { cout « "Klasse: " « typeid(typ).name() « endl; *2 *2 Hier wird de< Typ der Klaue dkrt- geßebefi. Die eoukte Ausgabe kann j.i nach dem System etwas queren, bool BType(Audioformatk tl, AudioformacSi t2) { return <typeid(tl) « typeid(t2)); "3 ) int main() •3 Hier verreichen wir, ob d*c zwei als Rrfcfffiz überlegenen P.u.imetcr vom selben Typ smd <truej oder rHdii {false) *t>9«i•:t»t« tmwn 418
MPSJFormat songj ("cest^mpS") t Ogg_FornLat song2("test.ogg") ; "4 der FunkVonnufrut um «Jen Typ 4er Kliw wwu^hen Type(songl); *4 Type(song2); ’4 bool check BType(songlt song2); ’5 if(check) ( cout « "Beide Typen haben dieselbe } eise { cout « "Die Typen der Klassen sind } return 0; Klasse\n"; "5 der FunkWns- aufruf um zu ver- gleichen, 0t> zwei Klassen vorn selben Typ und unterschiedlich^”; Das Programm bei der Ausführung (die Ausgabe kann sich auf den Systemen etwas unterscheiden): ovorride und final An dieser Stelle sollte ich vielleicht auch noch kurz auf die zwei neuen Idcntifzier Override und final hinweisen, die mit C++11 eingeführt wurden. Mit diesen Identifizierern kannst du sichergehen, dass Element funkt ionen auch wirklich eine virtuelle Elementfunktion der Basisklasse überschreiben und somit damit auch die vir tuelle Elementfunktion nicht aus Versehen überschrieben wird, wie dies In der Vergan- genheit gerne mal passiert ist. Hört sich blöd an. daher ein einfaches Beispiel dazu: 416 [if
dass Papa { virtual void huhu(float); >; dass Sohn : Papa { virtual void huhu(int) ; *1 }; '1 Sohn: :huhu beabsichtigt hier dir Vcfucxi der ßausktwr Papa aj ubwchrctben. Aber well cs $Kh hier um unterschiedliche Schnittstellen MMelt. wird eine zweite virtuelle ElemerMfunktion erstellt. Mit C++11 kannst du dieses Problem jetzt ganz einfach wie folgt lösen: dass Papa { virtual void huhu(float); >; dass Sohn : Papa { virtual void huhu(int) override; ’1 •1 0« Attribut override ruft den Compiler ins Spiel1 Der prüft jetzt, ob es In dec Basisklasse eine virtuelle Elemenlfunktion mit derselben Signatur gibt Ist dies nicht gegeben, iid sich der Compiler (wie in diesem Fall) eine entsprechende Fehlermeldung ausgeben. Desweiteren gibt es noeh einen zweiten neuen Idcntifier in 0+11 mit final, womit die virtuelle Elementfunklion nicht in der abgeleiteten Klasse überschrieben werden kann. Du kannst final sogar soweit verwenden, dass es nicht mal mehr möglich ist, von einer Klasse abzuleiten. wenn du diese mit final markte dass dass Papa final { ); Sohn : Papa { } 1 * '1 Der Compiler wird dies* Ableitung mit einem Fehler quittieren, di Papa mit final markiert wuide, ist keine Ableitung mehr möglich. dass Papa2 { virtual void huhu() >; final; ’2 ‘2 Hier markieren wir die virtuelle Elementfunktion m tt final dass Sohn2 : Papa 2 ( void huhu(); >; 3 ** *3 ... und wh jtzrn die^r vcm drm jfl Überschreiben di<*W Elcmcntfunk- tion hie/ Orr Compiler quittiert auch diesen Verweb hier. dfe W virtuelle Erementfonktion zu überschreiben, mit einem Fehlet. 417
So, die Vererbung haben wir damit komplett abgeschlossen. Jetzt bleibt mir nur noch zu sagen: Viel Spaß mit den neuen Fähigkeiten. Setz sie klug ein! Hierfür bekommst du den Vererbungs- meister-Pokal überreicht! 418 Cif
—ZWÖLF- ,, Templates Ausstechformen für die Plätzchen Dass er In C++ auch eigene Ausstechformen machen kann, erinnert Schrödinger an Weihnachten, wo seine Freundin Immer die trockenen Dinkelplätzchen macht, die sich im Mund wie Sägemehl anfühlen und ohne viel Glühwein gar nicht so richtig runtergehen wollen. Aber da diese Templates als bessere Alternative für «define-Makros über den Ladentisch gehen, macht ihn dieses Thema wirklich neugierig.
Da C++ja so zickig ist, was die Behandlung unterschiedlicher Typen betrifft, kann es echt nerven, wenn du für jeden Typ einen eigenen Algorithmus (Funktion} schreiben musst. Sieh dir hierzu folgendes Beispiel an: int weristGroesserfint vall, int val2) { *1 lf(v«U > val2) { return vall; > Ä return va!2; Äi iS,, > iwlr char weristGroesser(char vall» char val2) ( *2 iffvall > val2) { return vall; > return va!2; } •1 Einen Löftel für den Integer .. 'Z... einen for char double weristGroesscr(double vall» double va!2> { *3 iffvall > val2) ( return vall; } return val2; > *3 ... einen weiteren Löffel für double string ueristGroesser(string vall, string va!2) { *4 lf(vall > val2) { return vall; « uod noch eiowi } tauten für String return val2; u '5 Ruft die Iflteger- Vetsion jirf. ‘6 Hie* wüd die char- Verskxi dutgeiufen. cout « weristGroesser(10T 11) « endl; ’5 cout « wcristGroesserCa'» ’b') « endl; *6 *7011 double-version cout « weristGroesserf10.11, 10.13) « endl; *7 420 7tu>ce
string strl(”aaa"); string strZfaab"); cout « weristGroesser(strl, str2) << endl; *8 'S Und dies hier, natürlich, ruft die string-Version auf. Das Beispiel zeigt dir jetzt vier Funktionen mit demselben Namen, derselben Funktio- nalität und sogar der Quelltext, also die Implementierung sind identisch. Je eine Ver- sion für int, char, double und string. Diese Tipperei (ich hoffe du hast das Listing noch nicht abgetippt) können wir uns jetzt sparen und eine eigene kleine Aus- stechform daraus machen. Ijctztcndlich ist ja die Form der Funktion immer dieselbe. Nur der Teig ist anders. Jetzt willst du wohl wissen, wie du eine solche Ausstechform für ver- schiedene Teige schreiben kannst? Zunächst brauchst du hierfür ein Stückchen Code, um dem Compiler Bescheid zu geben, dass du hieraus eine Ausstechform machen willst. Dies kannst du mit folgendem Präfix einleiten: template <typenatae T> Der Parameter T steht hierbei für den formalen Datentyp, den du in der Definition der Funktion verwenden kannst. Der Name des formalen Datentyps muss allerdings nicht zwangsläufig T lauten. Du kannst hier fast jeden beliebigen Namen verwenden, aber in der Praxis wirst du hierfür häufig Mr. T dafür sehen. (HitTter£rundinlö| In vielen Programmen findest du übrigens an dieser Stelle auch die Zeile -^.Also kann hier anstelle des Schlüssel- wort dass verwendet werden. Beide Varianten sind identisch. Allein das Geburtsdatum von typename ist jünger als das von dass Taapl.it*? 421
Folgendermaßen kannst du jetzt deine Aus- stechform für eine Funktion definieren: template <typenan»e T> T Ausstechforra(T vall, T va!2) { return vall ♦ va!2; > Den formalen lyp T kannst du jetzt für alle erdenklichen Datentypen verwenden. Zum Beispiel: cout « Ausstechfonnf123.456» 234.567) « endl; //double cout « Aussccchforrn( 123456» 234567) « endl; // Int (Zc'leil Du musst aber wissen, dass die Definition deiner Ausstechform noch lange keinen Sommer macht, sprich Maschinencode erzeugt. Erst wenn du deine Ausstechform mit einem bestimmten Datentyp verwendest, wird auch eine Version für diesen Datentyp als Maschinencode hinterlegt. Eine solche Ausstechform wird also auch instanziiert. | । Wenn du in deinem Programm also keine Ausstech- form mit dem Typ char verwendest, wird auch keine [solche Ausstechform erzeugt! Verschiedene TbIq [Achtung} An dieser Stelle muss ich dich auch gleich warnen. Beim Auflösen deiner Ausstechform wird dein Compiler niemals eine interne automatische Kon- vertierung durchführen. Wenn deine Ausstechform also zwei formale Parameter mit Mr. T definiert, und du verwendest bspw. für den einen Parameter den Typ int und für den anderen den Typ long, meldet der Compiler einen Fehler. In solch einem Fall musst du eine explizite Umwandlung durchführen. Alternativ kannst du aber auch eine Instanziierung eines bestimmten Typs erzwingen. Wie das geht, zeige ich dir noch. Wenn du für deine Ausstechformen unterschiedliche Typen benötigst, brauchst du einfach nur mehrere formale Parameter zwischen tcmplateo zu schreiben. Natürlich mit einem anderen Namen. 422 zmnf
"1 Hkr konru-n mit TI und T2 zwei ver&Chredtne formalr Datentypen verwendet werden Natürlich Ht e$ auch hiermit «neulich und mcht faHch. würden 2wi?i Argumente vom selben Typ übergeben. Hierzu ein Beispiel einer Ausstechform mit unterschiedlichen Parametern: template ctypenaae Tlt typename ' void MuttiAusstecherfTl vall, T2 cout cout 1 « "Mein Teig : " « vall « "Mein Teig : ” « val2 T2> ‘1 ! val2) { « endl; *2 : « endl; *2 "2 Unsere Funktion macht im Grund« nichts anderes, ah den Inhalt der übergebenen Parameter auszugeben Mit der Funktion typeid( ) ^hatten wir ja schon) wäre cs auch möglich, gleich den Datentyp zu eemrttelrtf verglühen '3 Hier wird die Funktion MuttiAusstecher(long, string) inrslanÄert. *4 Andersherum, das heißt mit MuttiAusstecher(string, long) geht da5 natürlich auch. long string sval("Hefeteig”>j HuvtlAu«stecher(lv»l, sval) MuttiAusstecher(sval, Ival) Ival - 1234; (H«ntrr<rundin*€l Die formalen Parameter lassen sich nicht nur als Parameter für die Ausstechformen verwenden, sondern davon lassen sich innerhalb der Ausstechform auch lokale Variablen anlegen Compiler diese Funktion erzeugt, dann wird dieser p auch hier durch den entsprechenden Datentyp Gute Frage! Bei der Übersetzung überprüft der Compiler zuerst, ob es eine reine Funktion gibt, die zum Funktionsaufruf passt. Gibt es keine entsprechende oder zu den übergebenen Argumenten passende Funktion, wird nach einem Funkti- onstemplate gesucht und bei gefundenem Gegenstück auch ver- wendet. Der Compiler bevorzugt also eine reine Funktion vor einer Ausstechform. Muss halt passen. h.pi.t.i 423
Plätzchen backen Jetzt ist cs an der Zeit, dass wir eigene Plätzchen backen. Natürlich will ich dir hier auch noch den einen oder anderen Tipp miigeben. damit die Plätzchen besser gelingen. Also auf zum Ausstrchen! IfiaCtCht AuljAfeel Schreib ein Funktions-Template statt der überladenen wcristGroesser-Funktionen, die wir im Büro verwendet haben. Die Ausstechform soll folgende Fälle überprüfen und immer den größeren der beiden Werte zuruckgeben. Eine einzige Ausstechform soll alle diese Fälle bewältigen: cout « weristGroesserf10, 1t) « endl; cout « weriscGroeaaerf’a', ’b*) « endl; cout « weristGroesserf10,11, 10.13) « endl; string str I ("aaza’’); atring str2("aabaa"); cout « weristGroesserfstrl, str2) « endl; Hm, hierzu würde ich folgende Musterlösung abliefern: cemplate «typenaae T> *1 T weristGroesaer(T vall, iffvall > val2) { *3 return vall; *4 > return val2; *4 * 424 CackteL zmölf
iNotül Es ist natürlich auch möglich, deine Aus- stechform als inline zu deklarieren. Dies wollte ich hier noch schnell erwäh- nen. Nur für den Fall der Fälle! (Achtung Verricht) Jein, du kannst die Funktion (nicht) einfach überladen. In unserem Beispiel würde eine Überladung keine Probleme mit sich bringen, Aber im rauen Programmierer-Alltag, wo der Code über viele Module verteilt wird, kann es unter Umständen zu Problemen kommen Wird nämlich in einem anderen Modul das Funktions-Template definiert, kann der Compiler nicht wissen, ob es sich jetzt um eine Deklaration oder eine Instanz des Funktions-Templates handelt. ICede beubtittn] Für solch einen Fall würde ich dir raten, eine Spezialisierung zu definieren Im Grunde kein großes Ding1 Du schreibst einfach . ü/rtS wr deine Überladung und setzt templateo vor die Überladung. So kannst du in der Praxis eine Spezia* lisierung von weristGroesser für Strings schreiben: template <> ‘1 string weristGroesser<string><string sl, string s2) { if( sl.sizeO > s2.size() ) { *2 return sl; *1 Setz dw vor dir über« Urfrnc FufikliQH. und du h.ivt elr»c SpenjHsi-crufig deines Furilctiorvs-Te/nplatcs weristGroesser für string } return s2; '2 Einen String-Vergleich auf<f>e Anzahl der Zeichen führen w>t mithilfe der Element- funktion string: : size () durch. T«»pi«t*5 426
iMotill Beim Schreiben eines Funktions-Templates über mehrere Module solltest du den kom- pletten Code, also die Definition dazu, in eine Headerdatei schreiben, damit die Aus- stechform in allen Modulen, in die diese Headerdatei eingefügt wird, zur Verfügung steht. Das macht Sinn, weil bei Funktions-Templates ja erst ein Maschinencode beim Aufruf einer Funktion mit den entsprechenden Typen erstellt wird. Dafür braucht der Compiler den Code für die Funktion! weristGroesser ftafafy JA 4Cz_. « *^.*1 Me/* /c^. oZts cout « weristGroesser(10T 10.1) « endl; ’1 cout « weristGroesserCA11 66) « endl; *2 *1 Fehler, da mit int und double zwei vErM.hied*n* Typen verwendet werden *2 auch faheh, weil hier char mit int ‘.ermiveht wird Ja, Schrödinger, das Ist eine gute Frage! Und tatsächlich gibt es auch hierzu eine Möglichkeit, in C++ die Template-Argumente explizit anzugeben. Das geht sogar einfacher, als du denkst. Du brauchst nur die Template-Argumente in spitzen Klammern hinter den Template-Namen zu stellen. Folgendermaßen kannst du die Geschmacksrichtung deiner Ausstechformen explizit erzwingen: cout « wer!stGroesser<double>(10, 10.1) cout « wcristGrocsser<char>('A*, 66) « « endl; *1 endl; *2 {Achtung/ Vorsicht) Natürlich gilt hier dann wieder alles, was bereits bei der Umwandlung von Typen beschrieben wurde. Also auch das mit dem Hammer auf die Schraube hauen! •2 Und ihr beide solhet vom Typ char win! M H*tr erzwingen wir. dAW eine Auwtechfarm für den Datentyp double generiert wird {Schwierige Aufgabe! Schreib ein Funktions-Template mit dem Namen sindwirgleich, womit du prüfen kannst, ob zwei Datentypen, die du als Parameter übergibst, vom selben Typ sind oder nicht. Gib true zurück, wenn die Typen gleich sind oder false bei Ungleichheit. 426 zv+cr
Folgenden Code sollte deine Ausstechform überprüfen können: String strl("aaza"); string str2("aabaa"); sindwlrgleichescrl, str2) ) { cout « "Beide Typen sind gleich\n"; } eise { cout « "Die Typen sind unterBchiedlich\n’'; } long Iwert = 111; int iwert • 111; if( sindwirgleichtIwert, iwert) ) { cout « "Beide Typen sind gleichen"; > eise { cout « "Die Typen sind unterschied lich\n*'; > Ich geb dir einen kleinen Tipp. Hast du schon den typeid- Operator aus der Headerdatei typeinfo vergessen? C>4ty( tcmplate <typcnacnc TI, typename T2> *1 i , 41 bool sindwlrgleich(Tl vall, T2 val2) { return (typeid(vall) typeid(va!2) ); "2 } ’1 die Parametefnamen T1 und T2r damit sich daraus auch Firnktiortw mit zwei veiuhie- dwiefi Datentypen imtajiiiieien lasten “2 Hier vergleichen wir praktisch nur die Rückgabew^rte des beiden Strings .iu1 Gleichheit, die vön de-r Ekjmentfunktion name ( ) ru den Typen vall undvalZ zuruckgegeben werden. S*nd diese beiden String*£leich (==), w»rd true. andernfalh f alse zuruckgrgcbrn. Tmmp]4t*T 427
Am Ende sind nur noch Krümel da... So, nachdem du den Umgang mit deinen Ausstechformen für Funktionen beherrschst, bist du für das Plätzchenbacken mit €+♦ gerüstet. Bevor du jetzt den ganzen Teller Plätzchen mit deiner Freundin verputzt, bitte ich dich noch kurz um Aufmerk samkeit für ein paar Fragen. [E intabe Aulgxbt^ Kannst du mir ein paar Vorteile von Funktions-Templates gegenüber gewöhnlichen Funktionen aufzählen? Der Prograin mieraufwand ist geringer, weil nicht iur jeden Datentyp eine extra Funktion geschrieben werden muss. w Der Maschinencode wird aus der Ausstechform erst erzeugt, wenn diese im Programm verwendet wird. "• Weniger Code bedeutet auch weniger Arbeit, wenn der Code nachträglich bearbeitet oder verändert werden soll, Auch die Fehlersuche gestaltet sich hierbei einfacher. Sehr gut1 Aber hinzufügen solltest du auf jeden Fall noch, dass solche Funktions- Templates eine wesentliche bessere Alternative zu #define-Makros darstellen. IKiafjch? Au1p;ab<l Du hast im Zusammenhang mit den Funktions-Templates auch die Spezialisierung kennengelernt. Zu welchen Zwecken kannst du eine solche Spezialisierung verwenden? * Wenn mein gewöhnliches Funktions-Template kein vernünftiges Ergebnis auf einen bestimmten Typ zurückliefen. wäre das, glaube ich, ein guter Anwendungsfall für eine Spezialisierung. ** Gibt es Im Funktions-Template Anweisungen, die mit einem bestimmten Typ nicht durchgefährt werden können, könnte ich auch eine Spezialisierung schreiben. IfcrlQhflunj/ltouflgl Du hast jetzt prima mitgemacht! Als Nächstes wirst du erfahren, dass du auch Ausstechformen aus Klassen schrei- ben kannst. Aber vorher solltest du dir jetzt eine Pause gönnen und deine Plätzchen verdrücken, 420
Bevor du deine Ausstecher für deine Plätzchen aufräumen kannst, will ich dir zeigen, wie du dasselbe auch mit Klassen machen kannst. Solche Klassen Ausstecher hast du sogar schon unbewusst, bspw. mit vector, verwendet. Oder wie glaubst du. ist es möglich, dass du ein vector mit beliebigen Typen an legen kannst? Hier geht es natürlich nicht umVCCtor oder andere Teile von der Standardbibliothek (früher auch STL 1 genannt), sondern darum. wie du selber einen Klasscn-Ausstccher, wie bspw. vector. schreiben kannst. Ja. das geht! £ i|M«ntrr$rundin*c| ft $TL (Standard Template Library) und auch andere Stream-Bibliotheken in C++ sind \ standardmäßig u. a. über solche Klassen-Templates realisiert. Zwar sagt man heute eigentlich nur noch die Standardbibliothek dazu, aber vielerorts wrrd noch der Begriff STL verwendet. Daher geb ich dir schon einen Ratschlag von vorneherein: Guck erst mal in die Standardbibliothek, ob nicht schon was Fertiges für dein Problem vor- handen ist, bevor du wieder übers Kuckucksnest fliegst. Das Prinzip einer solchen Klassen-Ausstechform entspricht im Grunde dem von Funktions-Ausstechformen. Zur Einleitung verwendest du wieder folgendes Präfix: template <typename T> Mr. T sieht hier auch wieder für den formalen Datentyp, den du In der Definition der Klasse verwenden kannst, damit der Compiler nachher weiß, dass es sich hier nur um einen Platzhalter für einen später noch fesizulegenden Typ handel!. Der Name muss hier natürlich wieder nicht zwangsläufig T sein. Und ja. auch hier kannst du stattdes- sen das ältere Präfix template <class T> verwenden. Folgendermaßen kannst du somit eine Klassen-Ausstechform definieren: template <typenan>e T> dass Schatztruhe ( prlvatei T schätz; public: void setSchatz(T s) { schätz = s; } T gctSchatzO { return schätz; } Hiermit definierst du ein Klasscn-Templatc SchatZtruhe<T>. Der Parameter T steht hier wieder für einen beliebigen lyp. 429
[Aclitunfl Auch wenn hier im Beispiel Schatztruhe die Übergabe eines Wertes (Call-by-Value-Verfahren) bei der Elementfunktion verwendet wurde, solltest du das in der Praxis nicht unbedingt so machen. Vielmehr steht auf der Packungsbeilage, dass du die Parameter eher über Zeiger oder Referenzen realisieren solltest Gerade wenn du (oder jemand, der dein Klassen-Template verwendet) vorhast, dieses Klassen-Template mit Objekten zu füttern, werden, abhängig von der verwendeten Klasse, gegebenenfalls relativ viele Daten auf einmal kopiert. In der Praxis wirst du auch wohl eher sehen die Definition der Element funk Honen als inline innerhalb der Klasse schreiben. Allerdings muss ich dich hier gleich warnen, die Definition der Elementfunkrionen eines Klassen-Templates außerhalb der Klasse sieht auf den ersten Blick recht chaotisch aus. Damit es nicht zu grotesk erscheint, verwenden wir die beiden Elenientfunktionen des Klassen-Templates Schatztruhe. Hier- zu die Syntax: tcmplate <cypcnamc T> *1 void Schatztruhe<T>::setSehatzfT s) schätz s; > template <typenarae T> *1 T Schatztruhe«T>::gecSchatz() { *2 return schätz; > < *2 *1 Natürlich wird airch hierzu wieder das Prjfix template <typename T> verwendet. Mr. T ist wieder der formte Pd/awtcf *2 Im Grunde i>t auch beim Elcmentfunklionskopf alles wir bei gewöhnlichen Hcmefitfuiiklionen Nur musst du natürlich die formalen Parameter oder ft(kkgabewerte mit T kennzeichnen Besonders wichtig ist für den Zugriff von 3üfte*halb, den Klassennamen mit zü verwenden (also Schatz truhc<T>). Innerhalb einer Klasse kannst du ohne Weiteres KlassenName statt KlasscnNiwe<T> verwenden. Außerhalb des Geltungsbereiches musst du diese spitzen Klammern (<T>) verwenden. Merk direinfach, bet den Klassen-Ausstechern entspricht die Angabe von KlassenName<T> dum Datentyp. Nur KlassenNacnen für sich allem ist dagegen der Template-Name? 430
Klassen-Ausstecher-Elementfunktion überschreiben Wie bereite bei den Aussiedlern für Funktionen kannst du ebenso für die Element' Funktionen von Klassen-Ausstechem bereits vorhandene Elementfunktionen für einen bestimmten Typ spezialisieren, also überschreiben. Hier eine solche Spezialisierung der Elementfunktion setSchatzf): template <typename T> ’1 void $chatztrvhc<T>::$etSchatz(T s) { schätz s; > '1 Das ist die i/rsprüngHche Element- funktion setSchatz() für unsere Klasse Schatztruhe<T> templateo *2 void Schätztruhc<atring> cout « "Das > war setSchatz(string s> { Spezialisierung von strlng\n'*j *2 Dm Itl nun emr Spezialisierung Schatztruhe<string> Solltest du also ein Objekt mit diesem Typ definieren, wird r diese Vers*on statt de/ Version at ztruhe<T>::setSchat z() verwendet. Natürlich, Schrödinger! Das kannst du auch mit den Klassenausstechern machen. Und zwar so: tcmplate ctypcnatnc TI, typenamc T2> dass NochEfnTenplate { private: Ti irgendwas; T2 einTyp;
Beachic dann aber bitte, dass du auch die dazugehörigen Elementfunktionen entsprechend mit der Anzahl der formalen Parameter definierst: template «cypename TI, typename T2> void NocheinTemplate<Tl, T2>: :e leinene funkt ion( ) { > Das Instanziieren eines Klassen-Ausstechers ist dann nur noch ein Kinderspiel. Hierfür musst du lediglich zwischen den spit- zen Klammem den dafür festzulegenden Typ als Argument angeben, der für diese Klasse generiert werden soll. *1 Hier wird zunäciiM eine Klawfl-Airsstcchforni für den Typ Schätztruhe«int> gc^iertcrt und dann ein Objekt mit dem Bezeichner integer angetegt. Jedes Auftreten wki T Wie auch schon bei den Funktion*-Ausstechern erfolgt die Instanziierung erst dann durch den Compiler, wenn die Klassen- Ausstechform zum ersten Mal mit einem entsprechenden Typ verwendet wird. Also bspw.: in Verbindung mit dem Bezeichner wnd ddbei durch ein JH»nter^rundinfo| int eiictrt Schatztruhe<int> incegert *1 Schatztruhe<string> str[5]; *2 *2 fetzt wird zuerst eine weitere Kl.i^5Cfl-Au5Mechforni für Schätzt ruhe<string> grrrrirrt und rin Kli^n-Affriy mit drm ßr/eichnrr Str. welches fünf Objekte speichern kann Hier wird jedes Auftreten von T durch String ersetzt E* sollte dir klar sein, dass jedes andere Ausstecher-Argument für Mr. T auch einen anderen Maschinencode im Speicher erzeugt. •1 Jrdrt Auftreten von T I ward durch int und von T2 durch double ersetzt. KocheinTemplate<ine, double* mixOl; *1 KocheinTemplate<char, float* mix02; *2 Wenn du mehrere formale Parameter hast, musst du natürlich auch die entsprechenden Argumente beim Instanziieren mit angeben: •2 Hier wird jede* Vcwkonimfn vpn T1 durch den Typ char und von T2 durch den Typ float ersetzt 432 CepHvl zwölf
*2 ElementfunktiDn, um ein Objekt mit einem Wert m mitUliMerm Klassen-Ausstecher in der Praxis Jetzt wird cs allerdings auch mal Zeit, dass wir etwas in die Tasten hauen, um ein wenig Code für die Praxis zu schreiben. Keine Sorge, wir machen hier nix Kompliziertes- Als Beispiel darfst du einen einfachen Ausstecher ftir eine Reihe von gleichen Datentypen schreiben. Exakt, m3 etwas meinte ich! Natürlich brauchst du hier nicht das Rad neu zu erfinden und schon gar kein neues vector zu erstellen. Mir geht es nur dämm, dass du mit den Klassen-Ausstechern warm wirst. JEiarache Aulgabtl Schreib einen Ausstecher Rcihe<T>, mit dem du Daten vom selben Typ in der Reihe speichern kannst. Fürs Erste reicht es aus, wenn du den Aus- stecher, wie abgedruckt, füttern kannst: cout « intis 1 Reihe <inc> intis(5); *1 for( int i=0j i<5; 1++) intisli).sctDatcndh « endl; '3 1 Generiert eine Kkrwe Rcihc<int> Reihe <double> doubl!»(5] for( int i»0; double d doublis(i). > for( int i-0; cout « doublis[i].getDatenf) « endl; '3 > 4 *4 Generiert eine Klasse Reihe<doublc> 4 i ♦ 1.5; secDacen(d); *2 i<5; 1**) { ’3 Elemeflctunkiion, um den Inhalt des Objekten amzu« geben 433
f-htr Reihe<T>, 1 Hier werden die Daten gespeichert "2 Hiermit wer- den die Daten mit Werten initialisiert und template <typenac>e T> Ti Relhe<T>::getDaten() { *3 return daten; "3 ... mithilfe dieser Template- ElementKmktion können wir den Inhalt ermitteln und zurückgeben lassen. Ti d); *2 template <typcnamc T> void Relhe<T>t :setDacen(const Ti dH daten = d; template <typenarne T> dass Reihe { private: T daten; *1 public: void setDatentconst Ti getDatenO; '3 }; ‘1 Dank des neuen Namens icinn« du jetzt Reihe_int statt Reihe<int> für den int Trmpl.iteJyp verwenden. typedef Reihe<double> Reihe_double; *1 typedef Reihe<inc> Reihe_int; *2 ff statt Reihe <int> lntis[5j Reihe_int intis[5]; M II statt Reihe <double> doublis[5J Reihe_double doublis(5]; ’2 Natürlich wird diese typedef-Technik auch ganz rege in der Standardbibliothek von C++ verwendet. So ist zum Bei- spiel die dir bekannte Template-Klasse string ein typedef aus basic_string<char>: typedef basierstring<char> string; r Kode bearbert«r>] In C++11 kannst du jetzt aber auch using anstatt typedef verwenden. Zum Beispiel: using Reihe^int - Reihe<inc>; using Reihe_double Reihe<double>; (Code beaibedenl In der Praxis wird mittels typedef gerne ein neuer Name für einen bestimmten Template-Typ erzeugt. Bezogen auf unsere Reihe<T> könntest du zum Beispiel folgendermaßen einen neuen Namen für einen bestimmten Template-Typ einführen: F *2 Gleiches giH hier auch für dun Tcrnpl.itc*Typ double Anstatt Reihe<double> tu verwen- den- kannst du )ctzt dank des typdef auch den Namen Reihe_doublc nehmen. Unser Beispiel mit der Reihe<T> ist natürlich bisher ein letaler Murks, weil du hier n<xh extern mii dem Indexope- rator die Anzahl der Elemente rinbrin- gen musst. Wenn du da an vector denkst, fallt dir auf, dass hier alles auf Autopilot steht. Natürlich bedeutet dies auch, dass du bei solchen Sachen die 434 zweif
dynamische Speicherrcservierung selbst implementieren müsstest. Aber so weit wollen wir hier jetzt nicht gehen. Ich will In unserem bekannten dir stattdessen viel eher zeigen, wie du ohne diesen Indexoperatorais Templa- te-Para mc ter deines Klassen-Templates einen Wen angeben kannst, um die Anzahl der Elemente in deiner Reihe zu instanziieren. Du kannst den Parameter gerne auch Nicht-T (Mr. non-type) nen- nen. Beispiel kann dieser Parameter aussehen, wie folgt: tcmplatc <typcnarac T. int MAX»5> *1 dass Reihe { - "1 Hier bekommt unter KUssen-Ausstecher } । A einen zweiten Parameter, der d»e CrtBe des zu erzeugenden Arrays eMhJH. Innerhalb unserer Klasse hast du somit eine Konstante f jr d.e Große des Anjyi levigekgt. Itf-oUd Den Default-Wert (wie im Beispiel mit MAX=5) kann man(n) oder frau setzen. Muss aber nicht unbedingt sein. Wenn man(n) oder frau keinen Wert beim Anlegen eines neuen Objektes verwendet, werden automatisch MAX-Werte erzeugt. Nein, nicht ganz. Auf Dinge wie Gleitkommazahlen oder einen char-Zeiger für den Nlcht-T-Parameter musst du verzichten. tSchwicrigt Schreib unser Beispiel mit Reihe<T> so um, dass jetzt auch die statische Größe des zu erzeugenden Arrays beim Instanziieren des Klassen-Aussteehers angegeben werden kann, also Reihe<T,Aniahl> Wird kein Wert verwendet, gibst du als Default-Wert 5 an. Ein Beispiel eines Kopfes mit einem Non-Type-Parameter hast du ja bereits kennengelernt. Überlade außerdem zur Übung den [ ] -Operator, wobei du auch auf einen Über- oder Unterlauf des Speicherbereiches prüfst. Okay, ich helfe dir ein wenig. Schau dir folgende Verwendung dieser Klasse an, damit du siehst, was dein Klassen- Ausstecher so alles können sollte: Reihe<lnt> intis; *1 for( int i"0; l<intis.getSizc(); { "2 intis.secDatenCi, 1); *3 ’1 H4er werden fünf Elemente von Reihe mit dem Typ int (T»int) instanziiert. ’2 Die Elcmentfunktion getSizcC) gibt den konstanten Wert mit der AnuN der Elemente zurück, die lür den initiinziieiten Typ gespei ebert werden können. } for( int i"0; i<lntis.getSize(); !♦*) { cout « intis.getDaten(i) « endl; *3 *3 Hier «eben die Elementfunkikmen, die ermögliche«», einzelnen Elementen einen Wert zuaiwei*en oder den Wert eines bestimmten Elementen abzufragen. Tuplit» 435
'4 Hie/ werden zehn Elemente sw Reihe mit dem Typ double instanziiert. Reihe<double. 10> doublis; *4 M for( int i-0; i<doublis,gctSizc() double dval 0.5 * i; doublis[i) - dval; *5 5 Damit aiMh dd.li funlrtwiert musst du Ingi sch rrwrivr den ( l -Operator überladen. for( int i"0; Kdoublis-getSize() cout « doublis[i] « endl; ‘5 6 Hier passiert ein Pulferübertaul, den du beim Überladen des | ) -Operators auch bemerken sollten ... cout « doublis(10] « endl cout « doublis[-l] « endl dasselbe gilt natürlich auch fyf einen Puflerunterlauf' Na gutr Schrödinger. Ich denke. wenn du die Lösung gleich siehst, wirst du fcstsidlen. dass nichts so heiß gegessen wird wir gekocht. Hier eine Musterlösung '1 hie/ unser Klassen-Ausstecher mit dem NorvType-Parameter int MAX. der per Dcf.iuh mit dem Wert 5 belegt ws rd template «typenaroe T, int MAX«5> *1 class Reihe { private: V T daten[MAX](O}; II O+ll-llk« *2^*^ public: void setDaten(T& d, int Index); *3 Ti 8^tDaten(inc index); *4 Ti operator[ ) (int Index) ; "S int getSizcQ const { return MAX; } *6 "2 unser Array mit einer konstanten Anzah (MAX: vor» Elementen template «typename Ts int MAX> void Reihe<T, MAX>::setDaten(T& d. int Index) { datenfIndex) d; 3 Oh* Templatc-Elcmenlhmklbon . braucht jetzt natürlich auch einen Indexwert, um ein bestimmtes Element im Array gezielt mit dem Weit d zu initialisieren. template «typename T, int MAX> T& Reihe<T, MAX>::gerDaten(int Index) { *4 > *4 Dasselbe gilt auch für diese Elcmefitfunklicin. welche nun ebenfalls einen indexwert benemgt, um den Inhalt eines bestimmten Wertes im Array auszugeben 438 tZ4C,^f l ZMtflF
return daten[Index]; tcmplate <typcnamc T, int MAX> Ti Reihe«!, MAX>::operator(J<inc Index) { *5 if(index>=MAX) { cout « "RufferüberlaufII!\n"; return daten(HAX-1]; > eise if(index<0) ( cout « "PufferunterlaufII!\n"j return datcn[O]; > return daten[Index]; > *5 Der überladene [ ] -Operator, mit dem auth auf giriert Über- und Unterlauf geprüft wird. In der Praxis solltest du natürlich ändert reagieren, als nur einen Text und das letzte bw. erste Element des Arrays auwugeben Hier wurde s»ch bspw. eine Ausnahmc- behandlung (Exception) anbieten Aber dazu spater mehr. •G Damit wird dir GroÄe des Anays (Anrahl der Elemente) zurückgegebetK die gespeichert weiden kann und mit der Konstante MAX festgetegt »st Taaplatei 437
kh «.igle, KticbeAlMm, $chw4Jrw4<drr Kirschtorte Klassen-Ausstecher in der Wohnung Im Gegensatz zu den Funktion; Ausstechern für kleine Plätzchen entsprechen die Klassen-Ausstecher schon eher einer ganzen Kuchenform zum Backen von leckerem Kuchen, Mooooment! Bevor du dich jetzt an die Arbeit machst, diesen Kuchen zu verdrücken, wollen wir nochmals überprüfen, ob du den Ansatz der Klassen-Ausstecher auch verstanden hast. 438 i Zuger
ItMich? Aulgabel Kennst du den Unterschied zwischen ClassA und ClassA<T>? Erstelle aus der folgenden Template- Elementfunktion eine Spezialisierung für vector Das sollst du für den Typ double spezialisieren: template <typcnarac T> void Cla$$A<T>: :ping(T! pl> T4t p2) { II - > ct, /c\ /c ///£/* e/oi&t.' template«* void Cla$sA<double>:;ping(double! pl »double! p2){ Prima! Gut gemacht! (Eiftfichr Auigab*| Was kannst du tun, um ein Objekt eines Klassen-Ausstechers zu initialisieren, ohne die Dreiecke in Form von <T> für einen bestimmten Typ verwenden zu können? Ycah, Schrödinger, du rockst! Mach weiter so. Auf zur nächsten Frage! Ich komme mir vor wie ein Quizmaster. 439
flinfjch» Aufglb») Du hast doch auch schon was von den Nicht-T-Typen als Template-Parametern erfahren, mit denen du einfache Grunddatentypen verwenden kannst. Welche Typen kannst du nicht dafür verwenden’ Korrekt, Schrödinger! Ich habe mir schon überlegt, ob ich dir die Frage andersherum stelle. [Zettel) Es ist übrigens auth möglich, nicht nur einen Default-Wert, sondern auch einen Default-Typ für T vorzugeben, Die Standardbibliothek macht auch hiervon regen Gebrauch. Folgendermaßen kannst du veranlassen, dass int als Standardtyp eingesetzt wird, wenn kein anderer Typ verwendet wird: template «typenaae T»lnt, int MAX-5> dass Reihe ( }; Reihe<> intval; *1 •— Reihe <float> float_val; *2 Reihe <double, 20> double_val; *3 .Reihe <100> gehtnichrso; *4 *2 Imtaruiicrt Reihe<float,5> '1 Per Default wird hier e»n Ausltecbcr m*t Reihe<int, 5> inUainjiicft. *3 Wdi hier imtaruiiert wird, dürfte wohl ein(eu<lite<i iReihe<double, 20>; '4 D.t. klappt natürlich nicht Im Grunde wrh.il cs v<h h«er nämlich wie mit den Ocfdult-PAfamctern bei den Funktionen (Code bearbeit»«] Natürlich gibt es für trotzdem eine Lösung, indem du hierbei mit Aliase-Templates und using (neu in C++11) einen entsprechenden Typen erklärst. Hier die mögliche Lösung dafür: template <int MAX> using {Reihe - Reihe<int, MAX>; II C++L1 iRcihe<100> hundert; II Reihe<int, 100> |Bel<>heurtt/L<!r|ueg) So, jetzt kannst du dich an die Arbeit machen, deinen Kuchen zu verdrücken. Das hast du dir jetzt richtig verdient. Du kennst dich jetzt zumindest mit den Grundlagen der Ausstecher-Programmierung von Funktionen und Klassen aus, Da die Regeln für die Default Parameter die gleichen wie die für die Funktionen sind, kannst du auch nicht hergehen und Folgendes bei der Definition des Kfossen-Ausstechers schreiben: template «typenamc T"lnt« int MAX> *1 ClftSS Reihe { } *1 Da hie« der Wert von MAX rechts neben T über keinen Dcfault-Wert verfugt, «st diese Dclinltlor» hier ungültig Später kommen wir nochmals auf das Thema zurück, weil dir die Standardbibliothek zum Glück für fast jedes Problem eine Ausste- < her-Lösung an bi eiet. Dies nur für den Fall, dass du jetzt vorhast, ein Haus abzureiften und wieder ein neues hinzustellen, 440 tepim tusir
—DREIZEHN Ausnahme- behandlung Der Schleuderst« fürtfen Schrödinger sammelt Immer mehr Erfahrung In C++ und merkt, dass es hin und wieder zu unerwarteten Situationen kommt. In diesem Kapitel erfährt er, wie er auf solche Ausnahmezustände mit einem speziellen C++ -Konzept (Ausnahmebehandlung) reagieren kann.
Die Behandlung von Fehlern wurde bisher noch stiefmütterlich behandelt. Im Grunde wirst du mit deinen jetzigen Kenntnissen hergehen und auf die Fehler mit if oder switch case reagieren. Also, dein bisheriges Verständnis einer Fehlerüberprüfung: lf( istdaeinFehler() ) II Argh! Hier ist ein Fehler aufgetreten II Panikl Was willst du jetzt machen *1 *1 An dieser Stelle h.Ht du jeUt wirr MjOglichkriten Entweder du brlcMt düi Programm ab. oder rfu machst einfach gar nichts, oder du erstellst eigene Fehlere ödes, mit denen in einer FunklHMi ein Fehlerwert überprüft und entsprechend reagiert wird. Die vierte Möglichkeit istr dass es keine gibt. Wollte nur mal testen, ob du noch bei der Sache bist. Das Problem an dieser Methode der Fehlerprüfung ist, dass du damit zum einen nicht auf alle Softwarefehler reagieren kannst und dass damit zum anderen der Code recht unübersichtlich wird, weil du die Fehlerbehandlung mit dem gewöhnlichen Code vermischst. In C++ hast du Jetzt mit einer Ausnahmebehandlung (engl. exception handling) die Möglichkeit, die Fehlerbehandlung aus dem gewöhnlichen Code zu .schneiden". Ziel dieser Spezial' behandlung von Fehlern ist es natürlich, dass du den Fehler zunächst erkennst und dann selbstverständlich auch entspre- chend behandelst. Der Vorgang ist einfach. Wir versuchen (engl. try) einen Code auszufbhren. Wenn dabei ein Fehler auftritt, werfen wir (engl, throw) eine Ausnahme. Diese geworfene Ausnahme fangen wir auf (engl. catch) und behandeln den Fehler entsprechend. Wie der Fehler behandelt wird, hängt natürlich von der Ausnah- me ab. die geworfen wurde. 442 o»c1?(hw
Guck dir mal folgende Abbildung als Beispiel an: Hi«r em htasiuht! Beupirl: An ftetandf n wvrdtn «fw Mediki/nrnte* geleitet. de* Proband negal» darauf, werfen wk einen Airs- nahnwutfand in die Runde, Abhängig von de* gewoefe^eo Ausnahme gibt « mehrere Möglichkeit ei^ das Problem abrufangen. Zu dieser Abbildung will ich dir natürlich auch noch einen Pseudocode zeigen: try { *1 HedikamentA(probandOl); > -1 catch(int whatsup) { *3 fl Fehler behandeln •1 Zw^cbcn dl«cm try Block steht der kilt>$che Code, bet dem was .Schlimmes' passieren könnte in unserem Fall wird darin d»e Funktion McdikatnentA( > aulgerufen. void MedlkamentA(Probandi p) { if( p.kollabiert()) { throw 19222; // Notarzt *2 eise if( p.crbrichtO) { throw 12345; // Hausarzt *2 > eise if( p.gestorbenQ) { throw -1; // Bestattungsinstitut *2 } . > • ’3 Bctm catcht ) - Block steht auch schon unter Fänger bereit, wdchrr »m Fall nnn Ffhlrrv drn grworfrr>rn Fehlercode Abfangt und *rn AnwcHungsbkxlc dann entsprechend darauf rcagicft. •2 In der Funktion MedikamentA ( ) Urhcn schnn dir Werfer mit throw brrr.t le nachdem ob und welcher Fehler aufgeVeten \ >st. wnd ein einfacher Integer geworfen. A mn t Fra-b-a h<n0 3 ur»9 443
Typ*—-. t */uf—." t "7 /c£_ t*-^fht.f catcht) ä-w^^C Tyf*—* ** Du kannst aut sich um all die verirrten Aus nah Gute Frage! Zuvor muss Ich natürlich noch richtigstcllcn. dass solche .Finger* auch einen speziellen Namen haben, und zwar Exception-Handler (oder Ausnahme-Händ- ler). Ein catcht ) allein kann nur einen bestimmten Typ abfangen, aber wenn du unter- schiedliche Typen abfangen willst oder musst, dann kannst du weitere catcht) Blöcke dahinter hinzufügen. Und um gleich deiner wahrscheinlich nächsten Frage zuvorzukommen: ein alternatives catcht) einrichten, welches len kümmert, für die es keine Behandlung gibt. In der Theorie somit also: try { *1 II Hier kommt dein kritischer Code hin, II der eine Ausnahme werfen kann } catche Ausnaht&eTypl bezeichnet ) { *2 II Ausnahmebehandlung für Ausnaht&eTypl } catch( Ausnaht&eTypZ bezeichner ) { *3 /1 Ausnahmebehandlung für AusnahtneTypZ } catch( ... ) { ’4 *1 In dem Glock fuhrst du den kritischen Code aus *2 Im Fall e»nes Fehlers werden hier alle Ausnahmen vom Typ AusnatuneTypl (darf ein beliebiger Datentyp teilt) abgelangen und behandelt. *3 Hier gilt dasselbe, nur werden hier d Ausnah- men .om Typ AusnameTyp2 (em beliebiger Datentyp! abgefangen und behandelt 11 Ausnahr&cbchandlung für eine unbekannte Ausnahme Bevor ich dich an die Tasten lassen kann, muss ich noch ein paar Anmerkungen zu den Ausnahmen loswcrden. Du solltest nämlich noch wissen, wie nach dem Werfen einer Ausnahme mittels throweln passender Fänger dafür ermittelt wind. Zuallererst wird natürlich der Reihe nach angefangen und mit der ersten catch Anweisung nach einem passenden Fänger (Händler) gesucht. Ob der Fänger sich dann der Ausnahme annimmt, hängt natürlich auch von der catch Deklaration, genauer dem Typ, ab. Der Fänger nimmt die Ausnahme an, wenn ... •4 Wird eine schwerwiegende Ausnahme ausgewogen, für die kern Fanget eingerichtet wurde, kannst du e>n catch mit den drei Punkten (auch Ellipse genannt] als Parameter einrichten und dann im B'ock abfangen. Somit kann man verhindern, dass sich ein Programm nicht <n einem Un- definierten Zustand vertiert, sondern sauber nvit der Standardfunkt^ terminate () beendet werden kann. 444 Kapitel »kCIZCMH
X] ... der Typ der Catch-Deklaration gleich dem Typ der geworfenen Ausnahme ist. 3 ... der Typ der catch Deklaration eine direkte oder indirekte Basisklasse vom Typ der geworfenen Ausnahme ist. Der optionale Ausnahme-Handler catch( . . . ) muss demnach natürlich als letzter länger angegeben werden. Würdest du diesen catchHändler als ersten angeben, würden alte dahinter definierten Händler nie mehr aufgerufen werden. In der Praxis werden solche Fehler gerne in einer gesonderten Fehlerklasse definiert. Im Fall eines Fehlers wird ein Objekt von ^dieser Fehlerklasse mittels throw geworfen und mit catch abge- fangen. Mehr zu den Fehlerklassen später. Wenn kein catch auf die Ausnahme passt, wird intern die Funktion terminate ( ) aufgerufen, womit das Programm beendet wind. Natürlich solltest du hier jetzt nicht den Ein- druck bekommen, dass ein Programm sich sofort beendet, wenn du in einem try-catch-Block eine Ausnahme nicht sofort behandelst. Da es nämlich möglich ist. dass cs den Stack aufwärts mehrere tiy-caich-B16cke gibt, kann eine Ausnahme auch noch weiter .außen* behandelt werden. A u mh»ntf 3 ur>9 445
Jetzt schmeiß schonl Jetzt wird es natürlich Zeil, dass wir mal Ausnahmen in der Praxis werfen und aulTan- gen. Hierzu bedienen wir uns zunächst einfachster Typen, um mal die Schlüsselwörter try. throw und catch in der Praxis zu erleben. Hier unser erstes Schmeiß-Fang-Listing: double schneisstnichwegf double, double “1 Hier lieht der double vall, val2; t ry Block mit dem bool inacbweiter = truej ein Fehler .uftretrn und der eine Aufnahme while (machwelter true) { werfen könnte try cout « "Wert A ; ein » vall; cout « "Wert B : ein » valZ; cout « "Ergebnis«" « schmeissaichwegtvall। v*12)‘ *3 Zucnt Überprüfen wir, ob der Nennen V1 gleicht) Irt. Tofft dict werfen wir den Fehlercode H3 Dasselbe überprüfen wir natürlich auch mit dem Teik-r v2 uod werfen >n dem Fall den Fehler- code 666. « endl; } ‘1 catcht int i ) { *6 cout « "Fehlercode: ” « i « endl; *6 machweiter • true; *6 } catcht ... ) < *7 cout « "Unbekannter Fehler._\n”; ’7 nachweiter false; *7 double Bchneissmichwegt double vl, double v2 ) { ’2 if( vl — 0 ) ( *3 throw 333; '3 } iftvl < 0) < ‘4 *4 Ist der Nenner negativ werfen wil em String Objckt throw stringfNegativer Nenner!!!"); "4 ) if( v2 ““ 0 ) ( '3 throw 666; ‘3 } returnt vl / v2 ); '5 *5 Die Division wird nur avsgefühn wenn in den Zeilen zuvor kerne Ausnahme geworfen wurde. 446 Kapitel »etfZCHN
’2 Die Funktion schme isssilchweg() ist unsere Gefahrenquelle Es sollte natürlich rxxh jngemerH werden, dass du hierbei nicht nur auf eine Function im try-Block beschrankt bist. Aber wenn du eint Gruppe von Funktionen hier reinparken kannst, sollten dirsc Funktionen doch schon gleichartige Fehler verursachen D»e Funktion selbst macht Mehls anderes, als zwei double-Werte, die du zuvor eingegeben hast, zu divxlwren. * Dieser catch fängt unseie Feh tert ödes ab, welche von der Funktion schmeissinichweg ( ) aufgeworfen werden, wenn einer der beiden Werte gleich 0 ttt, und gibt den Fehlercode 333 oder 666 aus. N.rturlKh könntest du hier auch dir Fehlercoden auswerten und entsprechend reagieren. Das Fijg machweiter belassen wir auf true weil wir ja den Fehler (bspw. e*ne D»vrsion durch 0) taube* abgetanen haben '7 Dieser catchßlock fangt nur die Ausnahmen auf, für dir kein Fänger (Händler) eingerichtet wurde In unserem Beispiel wJkrc das die Ausnahme, wenn de< Nenner Hemer als 0 ist und wir einen string aus der Funktion schmc issmichweg () werfen, für die wir ja keinen gesonderten Farmer (Händler) eingerichtet haben. D.i wir bei einem unbekannten Feh er nicht sicher sein können, seticn wir machweiter urf f al SC und beenden das Programm zur Sicherheit Das Programm bei der Ausführung: Ä Weifen und Farx^rn Schroedinger $ ./schwelssmichiacg wert A : Jö Wert B : 15 Ergebnis - 0.666667 Wert A : 8 Wert 8 :CQ Fehlercode: 666 Wert A C0~~2> Wert 8 ; 9 Fehlercode: 333 Wert A Wert 8 : 10 Unbekannter Fehler... Schroedinger S Die fehlerhaften Eingaben wurden hier rot elngekreHt... Was passiert danach...? Wie du an dem Beispiel eben schön sehen konntest, kannst du nach dem Abfangen einer Ausnahme mit catch jederzeit mit der Programmausfühnmg fortfahren. Sobald du eine Aus- nahme in einem catch Block behandelt hast und dabei keine weitere Ausnahme geworfen oder gar das Programm beendet wurde, fahrt das Programm mit der ersten Anweisung hinter den catch Blöcken fort. Natürlich kannst du mehrere try-Blöcke mit catch- Ausnahme-Handlem verwenden, um in verschiedenen Programm- teilen unterschiedliche Fehler zu behandeln. Theoretisch kannst du auch innerhalb eines try-Blocks weitere try Blöcke verschachteln. Sinnvoll eingesetzt, kannst du damit schon eine Vorbehandlung von möglichen Fehlern im inneren try Block durchfuhren, um diesen dann von einem anderen catch Ausnahme-Handler im umgebenden try-Block nachzubehandeln. AmihMbl h*n<J3 wng 447
Dazu ein vereinfachtes, umgeändertes Beispiel vom Listing zuvor, welches dir zeigt, wie du try-Blöcke verschachteln kannst: double schmcis$michwcg( double, double ); void Input(double ival); — *1 det äußere double veil, val2; try-8tock bool machweiter true; ’2 der innere try-Ölock while(machweteer true) { try { *1 try { *2 cout « "Wert A : Input(vall); "3 cout « "Wert B : "; Input(val2); *3 > ‘2 catcht int i ) { '4 throw; *4 > *4 cout « "Ergebnis“" « schiDeissmichweg(vall, val2) '5 « endl; > M catch( int 1 ) { *6 cout « "Fehlercode: " « i « endl; *6 } '6 catcht < cout « "Unbekannter Fehler._\n"; machweiter = false; . } } return 0; *3 Im inneren try-Block uhrrprüfan wir quasi dir Eingabe von dm beiden Werten mit der Funktion input ( ) ist einer der beiden eingegebe- nen Werte gleich 0. schmeißen wird eine Ausnahme (hier den Integer-Wert 666). M Falb eben Im inncfcfi try Block eine Ausnahme geworfen wurde, wird diese natürlich vom dazugehörigen catch-Händler aufgefangen und behandelt. Vielleicht erscheint es dir seltsam, dass kh h$er erneut ein leeres throw innerhalb des Catch-Blocks verwende. Damit mache ich nichts anderes, als dir eben aufgefangenen Ausnahmen erneut auwuweden. um diese dann vom äußeren try-Bkxk behandeln 2u lassen *6 Dieser äußere catch Händler fangt im Fa I eines Fehlers auch die Ausnahme vxxn inneren try Block auf und gibt den Fehlercode aus. double schm£iss®ichweg( double vi, double v2 ) { *5 if(vl < 0) { throw string("Negativer Nenner!!!"); return( vl / v2 ); mit den Werten alles in Ordnung war und keine Ausnahme geworfen oder in unserem FjII wieder ausgeworfen wurde, wird die Funktion schmeissmichweg () im äußeren try Block m>t der Berechnung ausgeführt, wo w Fall e>nes Fehlers ebenfalls wieder eine Ausnahme geworfen werden kann. 448
void Input(double &val) { *3 ein » val; if( val — 0 ) ( *3 throw 666; *3 > Im Beispiel wurde qu^si die Eingabe von der Berechnung getrennt und in zwei unterschiedlichen try-Blöcken verschach- telt und auf Ausnahmen hin überprüft. Die Ausführung des Programms hat sich hierbei gegenüber der vorherigen Version nicht grundlegend geändert. Nein, natürlich nicht! Du kannst selbstverständlich die Ausnahme auch im inneren catch Händler auftangen und behandeln. Wenn allerdings beim äußeren try Block gleiche Typen von Ausnahmen aufgefangen werden, kann cs sinnvoll sein, diese ein- fach weiterzuwerfen und dort zu behandeln. Auch wenn es logisch sein sollte: Die Anweisung eines throw ohne weitere Argumente kann nur innerhalb eines catch- Blocks erfolgen! lc£_ inputO « try“^/e?t>^ F^— schmeissmichwegC) ? Völlig richtig! In dem Beispiel hätte man auch auf den inneren tty Block verzichten können. Du hast gut aufgepasst! In diesem Beispiel hätte das nur wirklich Sinn gemacht, wenn wir uns tatsächlich um die Eingabe mittels ein und nicht um den Wert gekümmert hätten. imihatbthändlung
Jetzt kennst du die einfachen Grundlagen, um Ausnahmen statt der traditionellen Fehlerbehandlungen in deinem Programm zu Implementieren, Mal sehen, ob du nicht nur mit dem Kopf nicken kannst, sondern ob darin auch was hängengeblicbcn ist. Wekhe drei Dinge brauchst du, um solche Ausnahmen einzurichten und behandeln zu können? Die Schlüsselwörter meine ich damit. w Zunächst braucht es einen Programmteil, in dem die Ausnahme ausgelöst wird. Dieser Teil wird in einem try-Block geschrieben. " Innerhalb des try Blocks wird natürlich irgendwas .Gefährliches“ ausgeführt. Tritt hierbei ein (schwerwiegender) Fehler auf. wird eine Ausnahme an die Auf- ruiümgebung geworfen. Hierzu verwende ich das Schlüsselwörtchen throw. " Damit im Fall der schlimmsten Fälle auch jemand die Ausnahme behandeln kann, benötigen wir natürlich noch einen Teil im Programm, der sich darum kümmert, diesen ablängt und dann behandelt. Dafür richten wir einen oder mehrere catch Blöcke ein. 460 Ueittl DftCXZCMH
|ti«fichr Aulfabel Okay, dann auf zur nächsten Frage. Was passiert mit einer Ausnahme, wenn du inner- halb eines catch-Blocks erneut throw ohne weitere Argumente aufrufst? Korrekt! Recht sinnvoll ist das dann, wenn diese Ausnahme aus einem verschachtelten try-Block ausgeworfen und dann von einem äußeren catch-Handler wieder auf- gefangen und behandelt wird. Andernfalls würde das Programm nur beendet werden. (Schwierige Atel gabel Ändere das Listing aus der Werkstatt um, Ist der Nenner gleich 0, wirfst du den Integer 1 als Ausnahme. Ist der Teiler gleich 0. dann schmeißt du die 2- Fang diese beiden Fehler ab, und informier den Anwender darüber. Wenn der Nenner negativ ist, schmeißt du nach wie vor den String ,Negativer Nenner" als Ausnahme raus. Dafür sollst du jetzt einen weiteren catch-Handler einrichten, der diesen String abfängt und aufdem Bildschirm ausgibt Auf eine Verschachtelung kannst du hierbei verzichten. O-Zj/!.' « « gleich 0!\n"; '2 Dasselbe m«iien wir, wenn der Teiler glekli 0 ist, und schmeKen dabei die 2. { *2 gleich — 2 ) Teiler eise if( i cout « int i ) { — I ) < *1 catchf if( i cout « "Nenner double vall, va!2; bool machweiter true; whilefmachweiter try { cout cout cout true) { *1 Oer Nenner ist gleich 0. dann wW dir Airtruhfrvc mil d-cm Integer-Wert 1 ausgewogen uM behandelt "Wert A "Wert B "Ergebnis3" « schnieissinichweg(vall, endl; «i ein ein » vall; » va!2; 0!\n"; val2) A u S n h 4 nd | wng 451
eise { *3 cout « "Unbekannter Fehler^.! \n"; machweiter false; } } catch( string e ) { ’4 cout « e « endl; *4 } catch( - ) < cout « "Unbekannter Fehleren"; machweiter false; } > return 0; ’3 Falls ein unbekannter Inieger- Wen ausgeworfen werden sollte, reagieren wird natürlich auch darauf Allerdings beenden wir hierbei zur Sicherheit die while-Schlcife indem wird machweiter auf false setzen. *4 Falls der Nennet negativ ist und wir ein 5t r ing’Objekt ah Ausnahme werfenr fangen wir dieses jeUt auch mi1 einem catch Händler ab und geben den Inhalt des Strings auf dem Bildschirm aus. double $chmcissmichwcg( double vl, double v2 if(vl < 0) { throw stringf"Negativer Nenner!!!"); ’4 > if(vl — 0) { *1 throw 1; *1 } if(v2 == 0) { *2 throw 2; *2 > returnf vl / v2 ); i > < ’2 Dasselbe machen wir. wenn der Teiler gleich 0 ist, und schmeißen dabei d<e 2 "1 Der Nennet ist gleich 0. dann wird die Ausnahme mit dem lnteger«Wert 1 ausgewoden und behandelt. JX Ifelohnung/Lösungl Nicht schlecht, Schrödinger! Ich sehe, du hast deine Haus- aufgaben gemacht und die Grundlagen zu den Ausnahmen verstanden. Jetzt kannst du ein wenig deine Füße hochlegen und entspannen Das Werfen von Fehlerobjekten zu verstehen, das wir anschließend behandeln, dürfte dir dann nicht mehr schwerfaNen. 462 c«t>U*L decucm*
Du hast cs bereits kurz erfahren, dass man in der Praxis eher eigene Fehlerklassen definiert und mit throw schmeißt. Was eine solche Fehlerklasse enthalten sollte, bestimmst allein du. Allerdings macht es natürlich Sinn, wenn deine Fehlerklasse unterschiedliche Informati- onen Ober die Ursache des Fehlers vermitteln kann. Okay, hierzu ein einfaches Beispiel, wie eine solche Fehlerklasse aussehen könnte: dass Fehler { string Info; public: Fehler( const stringEt str ) : Info(str) {} const strfngk geclnfot) const { return Infos } >; Ganz genau! Eine Fehlerklasse kann, wie andere Klassen auch, Daten und Element- funktionen enthalten. Besonders wichtig ist es natürlich, auch die Informationen des Fehlers im Objekt zu speichern. Aber hierzu vielleicht noch ein paar Hintergntndinformat ionen. was denn so passiert, wenn ein Objekt geschmissen wird. Wenn es passiert ist. dass throw anftngt, ein Objekt zu schmeißen, wird in der Regel ein vorübergehendes (temporäres) Objekt vom Typ des throw Ausdrucks erzeugt. Ist der geworfene Typ also eine Klasse, wird der Kopierkonstruktor aufgerufen. Das Fehlerobjekt wird ja während der Codeausführung In einem try-Block geworfen. Bevor also der Fehler behandelt wird, werden noch sämtliche Aufräumarbeiten auf dem Stack durchgeföhrt. die übrig geblieben sind seit Eintritt in den try Block. Das ist praktisch, weil somit alle lokalen, nicht-statischen Objekte und Speicherleichen automatisch zerstört werden. Dieser ordentliche Ruckzug wird auch als Stack-Unwinding bezeichnet. Erst nach diesem Abbau des Stacks wird zum passenden Fänger (catch Händler) gesprungen, welcher das temporäre Objekt gefangen hat. Natürlich sollte klar sein, dass auch das temporäre geworfene Objekt nach Beendigung der Fehlerbehandlung im catch-Handler ebenfalls wieder zerstört wird (wieder Stack-Unwinding). 463
Um jetzt auf die Klasse Fehler und unser Rechen- beispiel zurückzukommen, so kannst du wie folgt die Fehlerklasse Fehler als Ausnahme werfen: •1 in alten drei Fallen wird. wenn eine der Ausnahmen gewogen wird, die entsprechende Fehlermeldung im Febleröbjekt Fehler gesichert und kann somit mit der £lemenl1unklion Fehler;:getlnfo() ausgegeben werden double schmeissiaichweg( double vl, double v2 } { if(vl < 0) { throw Fehler("Negativer Nenner!!!"); ’1 > if(vl — 0) { throw Fehler("Nenner gleich 0!!!"); '1 if(v2 — 0) { throw Fehler("Teiler gleich 0!!!"); *1 return( vl / v2 ); Und wenn wir schon dabei sind, findest du hier noch den Codeausschnitt, der dir zeigt, wie du mit der Ausnahme des Fehlerobjektes Fehler abfangen und die Informationen darin ausgeben kannst: - double vall, val2; bool machweiter true; while(machweiter true) try { cout « "Wert A : cout « "Wert B ; cout « "Ergebnis“ « endl; > < ein » vall; ein » va!2; « schnseissmichwegfvall, val2) M Hier fangen wif die ÄAiUMhme vöiTi Typ Fehler ab, und... “2 ... hier geben wir lnkwniJl»onefi au%, warum die Ausruhme gewoden wurde. catch( Fehler e ) { *1 cerr « e.getlnfoO « endl; *2 } 4H-nter$rundin*o| Aufbauend auf eine solche Basisfehlerklasse, kannst du natürlich durch Vererbung weitere Fehlerklassen bilden. Das ermöglicht es dir, mit der ßasisfe hie rlc lasse die üblichen Fehler zu behan- deln und mit den abgeleiteten Fehlerklassen auf spezielle Fehler cinzu£(chcn. 464 tmiztHW
Schmeißen mit ganzen Klassen Okay, dann wollen wir mal eine ganze Klasse in der Praxis schmelzen. Nehmen wir als Beispiel gleich mal deinen CD-Wechsler, den du mit Gewalt füllen, willst, obwohl er schon voll ist. Ich kann das einfach nicht mehr mit ansc' hen, wie du das Gerät misshan- delst. Lass uns doch gleich eine Klasse dazu erstellen. Hier die Klasse CDWechsler zum Ausstechen. Einfach aus- gedrückt, handelt es sich letztendlich um ein Array-Template template <class T.int n> dass CDWechslerf private: T cd[n]; public; int get_size() const { return n; } Ti operator[)(int 1); }; template <class T, int n> Ti CDWcchslcr<T, n>::operator[](int 1) ( if< i < 0 ) < ‘1 cerr « "CD-Wechsler ist leer\n"; *1 return cd[i*l ]; *1 } eise if( i > n ) { "2 cerr « "CD-Wechsler bereits vollen"; return cd[i-l]; "2 > return cd[i ]; } 2 *1 Hier wird versixiiL eine CD Jtu$zuwerfen. obwohl wh gar keioe CD rotbr im Laufwerk befindet. Quan wrid liier dw Puffer unterUulen. *2 Dasselbe in Grün. Hier wird versucht, eine weitete CD rei nm stecken, obwohl der CD’WectiUer bereits voll ist. Dito, nur ein PuHerüberhuf. Kumah eü<h*nd 1 465
(Schwierige Aufgabe) Erstelle eine Fehlerklasse Overflow, die geworfen wird, wenn keine CD mehr im Wechsler ist oder der Wechsler bereits voll ist. Exakt das meinte ich! Okay, ein paar Tipps, überlege dir einfach, was fiir Informationen du in deiner Fehlcrklassr speichern und wir du diese im catch-Handler übergeben willst. Okay, ich helfe dir mit der Klasse Overflow, die wie folgt aussehen könnte: dass Overflow { ‘1 Hier wird die Poiition gespeichert, an der der Untrr«/Q herlauf statt« gefunden hat. private: Int Index; *1 *2 Damit schwiftcn wir die Ausnahme mit Ovcrflowt i) public: Overflowf int i ) : index(l) {} ‘2 int getlndexO const { return Index; *3 Die Elemendunktikxi benötigen wir, um d«e Position des Unter-Ä)berlaufc auszugeben. >; Okay, aber die Implementierung dieser Klasse bei der [ ] -Überladung und das Ein- richten mittels try und catch musst du jetzt selber hinbekommen. template <class T, int n> Ti CDWechsler«T, n>:: operator [){int. 1) { •1 Hier überprüfe ich if ( i < 0 | | i > n ) { *1 throw Overflow(l); '2 > return cd [ £ ]; zunächst, ob ein Unter- oder Überlauf von i itJttgeluoden hat. *2 Trifft dies tu, whmeiße ich das Fehlerobjekt Overflow mit dem Indexwert i .ws. 466 Beitel DRCIZtMg
CDWechsler<Btringf 5> Sammlung; *1 Uwf try-BfocknM drm knU$eben Code, in dem e»ne Ausnahme geworfen werden könnte. ’Z Hier habe kh nmt Abs-cht zum Testen e nen Purfe/flbeftamf eingebaut. try { *1 fort Int 1 0; 1 < Sammlung.get_slze(}; 1++) { *2 cout « "Titel der CD: 8«Bne(cln, S»™lunS(l)>, ÜMWi } 1 wird Mer die Ausnahme mit dem } ♦! catch-Händler aufeeiangeri. catch( Overflowfc e ) { *3 •4 Hier w^rd dann über d«c Elementfunktlüh Overflow::gctlndcx() l e Position angegeben an der ein Oberfaul stell- gefunden hat. cerr « "Unter-/Überlauf an Pos. " « e.getlndexO '4 « endl; IZeHcll Für die Verwendung eines CD-Wechsler würde ich dir hier einen typedef oder using (C++11) empfehlen. Bspw. so: using CDWcchslcr5fach CDWechslcrcstring, 5>| using CDWechslerSfach = CDWechsler<string, 8>; CDWechsler5fach Sammlung; Ä ,*n* f>htakUii»n werfen und langen Schro^dinger $ ,/cdwech$ler Titel der CD; Schrödingers lieblingshits Titel der CO: Charts 2012 Titel der CO; Charts 2011 - »Oi Titel der CO: Pop-Songs 2010 Titel der CO: Rack-Sanpier Titel der CO: Unter-/Überlauf an Pos. S Schroedlnger S 0 * Klatutcho Puff* ruber lauf, mit der Autruhmv-Brhandlung gfwöff-rr», aufgelangen u^d behandelt (trkdif'l Wow. bin beeindruckt, Schrödinger!!! Zwar hapert es noch an der Umsetzung einzelner Dinge, aber wenn du etwas implementieren sollst, bist du echt fit. Bin stolz aut dich! Ausn f hat-b-q h jn<j] un^ 4S7
un mit Klassen Prima, nun kennst du schon den Löwenanteil rund um die Ausnahme-Behandlung. Zur Sicherheit muss ich dich natürlich vor deiner wohlverdienten Pause noch abfragen. um zu sehen, ob du auch das mit den Fehlerklassen verstanden hast. Was versteht man unter einem .Stack-Unwinding"? (Schrödinger itolj.J «-/sc? Wahnsinn! Die Erklärung war ja richtig professionell von dir jetzt, super! Weiter so! 468
IC«fjchr Auigib*! Was braucht eine Fehlerklasse alles? tc^i fc^i sc/c^ie. /k'A£j"sx^r hjS <-*?« e« ha^r/"^>cr/C £'kk, r^ASS Scr/c^g £&£<?->"!4-^. Ft^tr tisn-faiL// Okay, kann man so stehen lassen! Pas mit den Daten der Fehler!;lasse ist insbesondere darum wichtig, weil ja nach dem Stack-Unwinding die lokalen und nicht-statischen Objekte zerstört wurden. CDWechsler Ganz genau, darum geht es hier auch. Indem du die Fehlerklasse innerhalb des public-Bereiches deiner Ausnahme schreibst, kannst du auch auf die Daten der Klasse zugreifen. Overflow (Schwierig» Aufgabe) Erweitere die Fehlerklasse Overflow vom Abschnitt zuvor um eine Elementfunktion, mit der die maximale Anzahl der Elemente zurückgegeben werden kann, welche in CDWechsler gespeichert werden können. Okay, ich helfe dir. Hier eine Musterlösung: template <class T,int n> dass CDWechslerf private: T cd(n); public: dass Overflow; int get_size() const { return n; } Ti operator!j(int 1); iiBnihMbthjndlung 459
dass Overflow { *1 privatet int Index; public: Overflow( int i ) : index(i).{} int getlndexf) const { return Index; } int getMaxO const { return n; } ‘2 >J -1 ’1 Dadurch d.n% dir FrfilerkldWr jeLrf innerhalb der Kl.iwc definiert wird, haben wir eine klasienspezifliehe Ausnahme etMellt, die nur innerhalb der Klasse geworfen werden kann “2 Des Weiteren ist jetzt auch <kr Zugriff auf Daten der Klasse möglich, weshalb vnr hier jetzt auf die maximale Anzahl von erlaubten Elementen von n der Klasse CDWcchslcr zugreden können. “1 Zugriff aut die jetzt klassenspezifische Ausnahme Bevor du erneut ins Straucheln gerätst, musst du natürlich auch wissen, dass der Zugriff auf diese Fehlerklasse jetzt außerhalb über die Klasse und den Zugriffsoperator erfolgen muss. Daher sieht die Verwendung der Fehlerklasse jetzt so aus: CDWechsler<string, J>> Sammlung; try { forffnt 1 • 0; 1 <• Sammlung.get_size(}» 1++) { cout « "Titel der CD: getline(ein, Sammlung[ij); catcht CDWech«lernstring, 5>:: Ovcrfl ow& cerr « "Max. erlaubter Index : *' « « endl; JBe*loh*ungrt.6sung| e ) { ‘1 e.getMax() *2 e.getIndex() Toll, Schrödinger! Jetzt hast du einen der komplexeren Abschnitte zu den Ausnahmen ohne Probleme durchgearbeitet. Im nächsten Abschnitt kannst du dreh ein wenig zurucklehnen, weil dort die Standardfehlerklassen behandelt werden. *2 Das funktioniert nur. weil du? Fehlerklasse Jetzt eine klassenspe- zifische Fehlerklasse von CDWechsler ist. 460 trut.i neciztHh
(Standard-)Ausnahmen Im Angebot Ausgehend von der Basisklasse exception (und dem Namensraum Std) sind auch Standard fehlerklassen für Ausnahmen im Angebot, die geworfen werden können. Hierzu ein kurzer Überblick zur Hierarchie der Standardfehlerklassen: Awtnihmwi-Hief*rchle def StAndMd1ehlerteliss«fl in C>4 Wie du auf der Abbildung schön erkennen kannst, werden die Standardfehlerklassen in logische Fehler <logic_error) und Laulzeitfehler (runt ime_error) unter- schieden. Der Unterschied dabei ist folgender: " Logische Fehler (logic_error) sind Im Grunde vermeidbare Fehler, die eine logische Ursache haben, wie ein Fehler im Prograntmablauf. » Laufzeitfehler (runt ime_error) sind Fehler, die während der Kontrolle des Programms jederzeit auftreten und nicht verhindert werden können. In der folgenden Tabelle findest du eine Übersicht zu den theoretisch vermeidbaren und somit logischen Fehlern der Klasse logic_error: Klissc Slim Header invalidargument üiiiullisig« Argument <scdexept> length_error maximale Größe iitwrichrilten <5tdexept> out_of_range ungültige Position (Adresse) <stdexept> domain_crror Werteberetttisfehler <stdcxcpt> im jhMbthjndlung 481
jHintergrunäinto) Neben den hier erwähnten Fehlern gibt es auch noch die Ausnahme ios_ base: t failure. welche von den unterschiedlichen Ein- und Ausgabe- klassen geworfen werden kann. Dazu aber später in der Werkstatt mehr. Jetzt auch noch eine Tabelle zu den nicht vorhersehbaren Fehlern der Klasse runtime error: Klasse Sinn Header range_error BctekhsuberKhreiliirig <stdexept> overflow_error Überlauf <stdcxcpt> underflow_error □nteriaul <stdexept> (AM***) Weitere Standardfehlerklassen, welche zum Stamm runtime_ error gehören und geworfen werden können, hier aber nicht aufgelistet wurden, sind regex_error (aus dem Header <regcx>), welcher bei einem Fehler mit regulären Ausdrücken geworfen werden kann, und system_error (aus dem Header <syste«n_error>), der bei einer Fehler- meldung des Betriebssystem ausgelöst werden kann. Wenn eine Ausnahme, ausgehend von der Basisklasse excep- tion, geworfen wurde, kannst du im catch-Block mithilfe der virtuellen Elementfunktion what () eine Fehlermeldung der Ursache ausgeben lassen. Die Elementfun küon gibt aller dings einen C-String zurück. Alle von exception abgeleite- ten Fchlerklassen definieren einen Konstruktor mit einem String als Parameter. Somit kannst du beim SchmeiKen solcher Ausnah- men diese auch gleich mit einem String initialisieren. Das ist ganz leicht, hier ein Ausschnitt, wie es geht: "2 Da isses passiert und cs wird die Ausnibnxe geworlen. "3 Hier kätschen wir die Ausnahme ab. throw out_of_range(*’Aaaah! Bereichstiberschreitung’*); *2 cry { II Hier könnte es passieren *1 > catch( out_of_range ie ) { *3 _/ ’i Da könnte es kritisch werden, drum zur Skhertieit in t einem try Block. cerr « e.what() « endl; *4 482 PECIZtMk *4 Ufsd hier spr« htji .vrr KLmum, w.u Ocnr. k/s war In dem Fall wird HAaaahf Bereichsüberschreitung“
Ebenfalls von der Basisklasse exception abgcleiiei, sind einige Standard- fchlcrklasscn. die von der Standardbibliothek selbst verwendet und geworfen werden können. Auch hier eine Tabelle zu den Ausnahmen, welche vom System geworfen werden können: Klasse Sinn Header bad_alloc Speicherfehler. Beispiehweise konnte ein mit new angeforderter Speicher nicht reserviert werden. <ncw> bad_cast E«ne Typumwandlung mit dynamic_CBSt schlagt fehl. <typeinfo> bad_typeid Wird geworfen, wenn der typeid-Operator einen falschen Objekttyp erhält. <typeinfo> bad_exception Wird bei Problemen mit der Ausnahmebc- handlung selbst geworfen. <cxccption> Im folgenden Beispiel stellen wir uns darauf ein dass new() keinen Speicher bad alloc werfen würde: erhält und somit die Ausnahme linclude <new> int *lPtr; try { IPtr new int[10000]; *1 > catch( bad allocfi e ) { *2 cerr « "Ausnahme bei new\n'*; *2 cerr « e.whatf) « endl; *2 '1 Oiese Spekh«- anfo«de<ung setzen wir in einen try. Block, Lilh es nicht genug zusammenhängen- den Speicher geben sol-’te. ’2 Gibt* keinen Speicher, fingen wir hier die Ausnahme der Standardlehlerklasie bad_alloc ab und reagieren entipw hend. Im Beispiel geben w»r nur eine rnformation dazu aos. in der Praxis konntest du hier versuchen, erneut etwas weniger Speicher anzufordern, wenn möglich. } Aumakf-freh jnU3un9 413
Wir probieren es aus In der Werkstatt steht die Praxis im Vordergrund, deshalb will ich dir auch einige Beispiele steigen, wann welche Standardfehlerklasse wo ausgeworfen und verwendet wird. Aho Achtung, Jetzt wird es praktisch! Logischer Fehlen out of range Diese Ausnahme wird geworfen, wenn ein Wert nicht mehr in einer gültigen Adresse liegt. Diese Ausnahme können wir aber auch für unsere eigenen Bedürfnisse verwenden. An dieser Stelle angekonimen. wollen wir nochmals kurz die Klasse CDWechs 1er aufgreifen, bei der du Jetzt deine selbst geschriebene Klasse Overflow entfernen kannst und stattdessen einfach nur OUt_of_range zu werfen und abzulangen brauchst. ST L-Standardklassen wie bitset, string oder vector werfen bspw. die Ausnahme out_of_range, wenn ein Index außerhalb des zulässigen Bereiches verwendet wird. 464 »«cizckw
Hier die entsprechenden Codezeilen dazu: linclude <stdcxccpt> *1 template <class T, int n> <Den Herder brauchen wir für out of rangc T& CDWechsler-cT» n>::operator [ J (int i) { if( i < 0 || i >- n > { throw out_of_range(’'Waaahl Über-/Unterlauf”); *2 > return cd[i|; > CDWechsler<string, 5> Sammlung; try { for(int i 0; i < Sammlung.get_size(); i++) { *3 cout « "Titel der CD: "; getlinetcln, Sammlung!i)); *2 Im Fall eines Unter-Äxter Überlaufs whmeißen wii jetzt einfach out_of_range mit einem pjjswnden Text für die EleTrntfunktipn what ( ) } catcht out_of_range ie) { *4 cerr « e.whatO « endl; *5 } •4 Dieser catch Mandler fa.nfct den Typ OUt_of_ ränge ab, *5 Und hier geben wr mi? what () den Text den wir beim Auswerfen an out_of_range übergeben haben Logischer Fahlen invalid argument Diese Ausnahme wird geworfen, wenn du ein falsches Argument verwendet hast. Standardklasscn wie bitset. deque. string oder vector ver- wenden diese Ausnahme auch. Sieh dir folgendes Beispiel dazu an:
try { bitset<8> setl( stringC'10100121")); "1 } catch( Invalid argument &e) { *2 cerr « e.whatO « endl; "3 } *1 Du muHt gar mcht nul wissen. wav bit$et jctTl ge-n.iu für r.ne $TL-Kl.i$se l$t. E$ reicht für dich ent mal aw, dass du weißt, dassbitset keine linieren Zeichen ab 0 und 1 kennt. Hier wird versucht. e»n St ring in ein bitSCt umiuwanddn. O»e Zahl 2 ist hier ülsch. und sonvt stellt das Ganze ein ungültiges Argument dar. “3 Hier wird auch ausgegeben, was d»e Ausnahme ausgelöst hat. Im Beispiel könnte dies bspw. bitset: M_copy_ f rom_ptr lauten. "2 Das hier ist unser catch Mandler, welcher ungültige Argumente auHAngt, Logischer Fehlen length_error Den Fehler werden wir jetzt nicht in der Praxis demonstrieren, weil das deinen Rechner ziemlich in die Knie zwingen kann. Diese Ausnahme wird geworfen, wenn ein Objekt erzeugt wur- de, welches größer ist, als maximal erlaubt. Dies ist bspw. der Fall, wenn du versuchst, einen string mir max_ size { ) +1 Zeichen zu stopfen. Die Ausnahme wird von den Klassen String und vector geworfen. • J 1 ' xr IfiMigllJ domain_error wird innerhalb der Standardbibliothek nicht eingesetzt und macht nur bei mathematischen Funktionen einen Sinn. Verwendest du bspw. die Quadratwurzelfunktion mit einem negativen Wert, dann ist diesem Bereichsfehler, weil diese nur für nicht-negative-Funktionen definiert ist. Dann kannst du die Ausnahme domain_error werfen und abfangen. 405 üpiltl DRCIZfMH
Logischer Fehlen ios_base::failure Zwar hast du die Ein- und Ausgabeklasse n, wie bspw. das Öffnen und Bearbeiten von Dateien, noch nicht ken- nengelernt, aber du kennst zumindest schon mal ein und COUt für die Ein-/Ausgabe, womit ich dir die Fehlerklasse ios_base:: f ailure vor die Füße schmeißen kann. Diese Ausnahme musst du allerdings vorher mit der Eleinentfunktion exceptionsf ) aktivieren und mit einigen Kennzeichen (engl, flags) .einrichten'. Um folgende Kennzeichen, die du auch mit dem Bit-weisen ODER verknüpfen kannst, handelt es sich dabei: Kennzeichen Sinn und Zweck ios::failbit Zeichen bei de/ Eingabe kennten nicht gelesen oder bei der Amgabc nicht ausgegeben werden. ios::eofbit Dateiende wurde beim Lesen erreicht. ios::badbit Fehler während der Eiri'Musg&beDperation im Puffer des Streams ios::goodbit keine Fehler ‘1 Hie/ richten wir die Ausnahinenmayce ein. In unserem Fall reicht es völlig; am, nur eine Aufnahme auf ios:tfailbit cinruftchtcn “2 Wird hier etwas anderes als cm Integer, bspw. ein Zeichen, eingegeben, wird d«e Ausnahme geworfen .. +3 .. und von unw/em catch-Handler abgeüngen Willst du somit überprüfen, ob z. B, ein einen korrekten Integer-Wert vom Benutzer erhalten hat. kannst du folgender- maßen eine Ausnahmebehandlung dazu implementieren: m int val; ein.excepcions (los::failbit); ‘1 try { cout « "Bitte Eingabe machen : ein » val; *2 } catch( ios_base::failure le) { *3 cerr « e.whatO « endl; INotill Die Laufzeitfehlerklassen rangc_error, undcrflow_crror und overflow^error wurden so implementiert, dass diese auch von dir geworfen werden können und nicht nur als Ausnahmen der Standardbibliothek. ♦87
ror 4x6 ardausnahme- Kontrolle Ul' Ich merke schon, dass dir die Standardfehler- klasscn nicht schwergefallen sind. Trotzdem dürfen die obligatorischen Testfragen zum Thema nicht fehlen. lEhrfachr Aufgabe] Ausgehend von der ßasisklasse exception. unterscheidet man zwischen logischen Fehlern und Laufzeitfehlern. Wie heißen die Klassen dazu und worin besteht der Unterschied? Prima! Vielleicht solltest du auch noch die Ausnahmen hinzufugen, die vom System geworfen werden können (wie bad_alloc, bad_cast,bad_ typeid oder bad_except Ion). 468 Capitel »tCIZCMW
[lintacbe Aufgab«) Was kannst du tun mit what ()? (Schwirrte Aulf-ab*! im folgenden Listing findet eine fehlerhafte Typumwandlung statt. Verbessere das Listing, indem du einen Ausnahme- Händler dafür verwendest. Ich sehe schon, das Thema überfordert dich nicht. Dann will ich mal eine etwas schwierigere Aufgabe aus dem Hui ziehen. Das fehlerhafte Listing: dass dass Vaterf virtual void efunef) Sohn : Vacer {}; <} }; Vater Sohn& 1 Hier fliegt beim Programm die SfClie/ung raus. Es wird std::bad_cast geworfen Fang diese Ausnahme ab v; s Vater try { Sohnk > catcht cerr v{ *1 cerr > dynamic_cast<Sohnk>(v); VL "2. und hiei der catch'Mand er, der die Ausnahme bad_cast abfangt. bad_cast &e) { ’2 « ttbad_cast abgefangen: « e.whatf) « endl; $ dynamic_Cast<Sohn&>(v) (*tl0h«iungA6|ung| Bin echt beeindruckt Das Thema können wir jetzt bleiben las- sen. Du hast die Ausnahmen mit Bravur genommen. Im letzten Abschnitt erfährst du nur noch ein paar kleingedruckte Dinge zu den Ausnahmen Bis dahin würde ich dir eine Pause empfehlen. Wie wäre es mit einem heißen Tee oder einem warmen Bad? * m h * n<J ] un$ 489
-Spszlf I katlon und nouxcuRi^k Hier war Ja ursprünglich das Thema Ausnahme-Spezifikation (Excepüon-Spczifikatlon) angedacht. Aber du hast Glück, weil das Thema jetzt mit dem neuen C+*11-Standard als .deprecated“ erklärt wurde. Wenn du vielleicht zuvor schon etwas davon gehört hast, darfst du das jetzt vergessen, und wenn du nichts darüber weißt, braucht dich das auch gar nicht mehr zu interessieren. IHintergrundinfar1' ' Falls du wirklich wissen willst, warum die Ausnahme-Spezifikation jetzt .deprecated" ist, kannst du folgenden Artikel darüber lesen (Achtung! Englisch!): http://w\vw.götw. ca/pubhCAifönt/m>ll22. htm n « Neu hingegen in C**11 wurde das Schlüsselwort noexcept \ aufgenommen. Da ja jetzt ein leeres throw ( ) bei einer Funk- w* tion „deprecated" ist, kannst du jetzt noexcept stattdessen \ verwenden. Wenn du eine Funkiion mit noexcept mar- \ kiersl, bewirkt dies ... dass die Funktion keine Ausnahme werfen darf. " dass das Programm nicht auf die Ausnahme reagiert, welche von dieser Funktion geworfen wird. In diesem Fall wird std:: terminate aufgerufen. Einfachstes Beispiel hierzu: void functlonf ) noexcept; ’1 |AbU«#l Du solltest auch wissen, dass durch das Schlüsselwort nocxpect alles zur Übersetzungszeit ausgewertet wird. Dadurch, so der Gedanke, stehen dem Compiler mehrere Möglichkeiten für Optimierungen zur Verfügung. Daher macht auch die Standard- bibliothek jetzt regen Gebrauch von noexpect, um mehr aus ihr herauszuhofen und deutlicher hervorzuheben. '1 Die Funktion darf keine Ausnahme weifen Tut sie es trotzdem, wud die Ausnahmebehandlung darauf nicht reagieren. auch wenn du einen spenellen Mandler dafür eingerichtet hast. Stattdessen wird direkt std: jtenninate aufgerufen 470 Capitel htCIZCMif
Selbst mit terminate() muss nicht gleich Schluss sein. Auch ein (unerwartetes oder erwartetes) terminate ( ) kannst du mit einem Händler ab' fangen. Das funktionier) ähnlich wie schon mit dem unexpected Händler, nur dass du hier die Funk dem set_terminate() zum Einrichten des Händlers verwenden musst. "1 t>« Ist dcf Code für den tcnninatc- Handler. Zuvor muss dieser ... linclude <exception> void hastalavistababy( ) { '1 // Quälcode für den tereninate-Handler } set_teralnate( hastalavistababy ); *2 •2 ... mit de/ Funktion set_ terminate () mflcrichtei werden. [HjnKtftfuftdiäfö] Die Funktion terminate () ruft standardmäßig die Funktion abort () zum Beenden des Programms auf. 471
Ausnahmen verweigern Jetzt weißt du. wie du Funktionen einschranken kannst, wenn diese eine Ausnahme werfen wollen. Im folgenden einfachen Listing kannst du sehen, wie du eine Funktion einschranken kannst, damit diese keine Ausnahme mehr werfen kann: Int keineAusnahae( int, int ) noexcepc; ‘2 Zur Demonstration packen rii dfn Funktionsaufruf in einen try Bkxk. Ausnahme werfen darf int val{0}; try ( val keineAusnahme(2,0); > catcht ♦ ) < *4 cerr « *Ausnähme geworfen...???" « endl } ‘4 Dieses C3tch () wird . in unsetem Bespiel aber trotzd-rm nicht aktiv wegen des Schlüsselworts noexpect. womit wir unsere Funktion keincAu$nahmc() markiert haben. int keineAusnahcoet int x, if( Ix || ly ) throw -1 return (x/y); Hier werfen wir einfach mal zum Test eine Ausnahme raus, wenn x oder y eben fake sind (was sich e?gibt, wenn einer der Werte 0 ist. und wir mit dem Aufruf von keineAusnahme(2, 0) auch provozieren) □ o$ Programm bei der Ausführung: ** 5<hrWd*n0er* S ./•«in Bitte teurer et rieben: -1 terwvnote culled without on octive exception Abort trop Schroedin^er I ./»in Bitte hkiflRer eingeben: 0 tcramate colled öfter throwmg an instance of 'int* Abort trap Schroedtnijer $ ,/wn Bitte tajnrrr «ingeben; 1W1 string geworfen Schroedlnfler S ./oln Bitte hMw®er eir-geben: 999 •ei neAus-noMe geworfen Schroedinoer S ./□in Bitte hfrjRwer eingeben: lt»Ö ID (&!! Schröfdinjer $ _ rrcT' Ja, völlig richtig, die Standardsachen werden nach wie vor eingehalten. Nur wird hier eben nicht, wie ohne noexcept, der catch() -Block aktiv, um die Ausnahme abaufangen. 472 c.ptt.i »«cizchh
Gtt *4h std:: terminate() -farfvaf ä6^*^*v,? Z>a *—*tr öjiczC A_«ts #*r// rtl-, A^,4X<>^ Set_terminate () ? Nattirllch kannst du das machen. Das soll auch Teil deiner nächsten Aufgabe sein, wenn du schon so fragst. {Schwierig«- A ufgabe | Erweitere das eben erstellte Listing um eine Funktion, welche unerwartete und hier wegen noexccpt unerlaubte Aus- nahmen abfängt, welche als Folge haben, dass std: :terminate() aufgerufen wird. 0^a/t das rc4. ! int keineAusnahme( int, int ) noexcepc; void terminator( ); *1 int niainO < int val{0}; “1 Die Deklaration wird verwen- det. falls das Programm mit std::terminate() beendet w»id. sot_terminatc( terminator ); *2 try { val = keineAusnahmeCZ^O); c2 Und wird die Funktion terminator () aR tcrmlnate- Handle/ eingerichtet, damit das 1 Programm nicht gleich unverzüglich beendet wird, wenn std::tenninate() k aufgerufen wird. *3 Da die Funktion mit nocxpect eingerklitet wurde, wurde* auch kein catch'Händler dafür eingerichtet. Füglich führt diese geworfene Aufnahme dazu, std::terminate() «jirfgeriHcn l wird. Und hie/ ... } catcht ... ) { cerr « "Ausnahme geworfen...???" « endl; } return 0; int keineAusnahmef int xt int y ) noexcept { if( Ix || !y ) throw -1; *3 return (x/y); ) ’4 kommt natürlich unser dafür eigens e»ngerichtetef terminate-Händler terminatorQ zum Einsatz!!1 void terminator ( ) ( ’4 cout « "Hasca la vista* baby?" « endl; cout « "Ausnahmen verboten!!!” « endl; Das Programm bei der Ausführung: ___________________________UmmiM _________ | SchroedtrHJtr $ ,/«ötn_____£ 81 tu Muaaee ttnflebtfi; ö Ne des. fi4te tch nicht ehrtet une^pected obpeformen Schroedinger $ ./«ein Bitte Nuwwer cingeben: -1 ilGsto Id visto Abort trop Schroedinger $ G Respekt, Schrödinger! Damit bist durch mit dem Thema noexcept Zur Belohnung solltest du was ordentliches Essen. Wie wäre es mal wieder mit Schnitzel und Pommes? scA.^ ... 473
Keine Erwartungen Ok. an dieser Stelle möchte ich noch ein paar letzte Worte bezüglich noexcept loswerden, weil cs halt doch noch neu Ist und du dich sicherlich fragst, wann (oder ob) du es verwenden solltest. Du weißt jetzt, dass du hiermit die Performance deines Programms verbessern kannst. Ebenso weißt du. dass hiermit knallhart ein Std: iterminate aufgerufen wird, um das Programm .abzuschießen". Das Grund verhalten einer gewöhnlichen Funktion ist es aber, dass diese eine Ausnahme werfen kann und du als Programmie- rer dafür verantwortlich bist zu reagieren. Auch dein Compiler verwendet implizit bspw. bei Mitgliedern einer Klasse noexcept, ebenso wie deine Standardbibliothek regen Gebrauch davon macht, Daher der einfachste Hinweis und Tipp gleich an dieser Stelle, bevor du noch mehr verwirrt bist; Bist du dir nicht sicher, was dir das neue Feature bringt oder ob du noexcept verwenden sollst oder nicht, dann lass cs einfach sein. Verwende das Konzept nur dann, wenn du weißt, warum du es tust1 Es ist zwar nur ein einfaches Wörtchen, aber darüber lässt sich mehr sagen als dir hier lieb ist. Eine sehr umfangreiche Einführung über noexcept kannst du auf der folgenden Webseite finden: h t tp://aknemi 1. wordpress. com/2011/06/ 10/using-noexcept/ Die Verwendung von noexcept wird auch als dynamische Ausnah mespczifikation (dynamic exception specifications) bezeichnet. ISeloMun^L'Jsu^cl Prima, Schrödinger* Hiermit verleihe ich dir den Orden für den .Ausnahme- Spezialisten"! 474 PtClZtH»
—VIERZEHN— Unterwäsche, 100 % Baumwolle, Doppelripp •I Die Standard - klasse string Schrödinger wei ]a mittlerweile, dass man einen String nicht nur als Unterwäsche tragen, sondern damit auch Text verarbeiten kann. In diesem Kapitel erfährt er mehr über diese Klasse und den Umgang mit Ihr.
Dass die Klasse String nichts mit dem knappen Lenden- schurz zu tun hat. solltest du bereits wissen. Wenn es um Text geht, ist string dein Freund. Und unser guter Freund hat eine Menge mehr zu bieten, als .nur* Text speichern zu können. Er verschafft uns noch viele weitere Möglichkeiten, den Text zu manipulieren. Hierfür kannst du etwa auf Dinge wie das Suchen und Ersetzen, Einfügen und Entfernen und noch einiges mehr setzen. Hier will Ich dir ein paar Möglichkeiten aufzeigen, wel- che einige dieser Funktionen bei der Ausführung zeigen. Auch wenn es dir bereits klar sein sollte, die Klasse String ist in der Headerdatei <String> deklariert, weshalb du diese bei Verwendung auch mit ein bin den musst. |Hintcr(rundinfo| Die Klasse string ist auch eine echte Container-Klasse der STL (Standard Template Library}. "7 Oer String wud mit zehn X gdullt Bei der Erzeugung neuer Strings stehen dir viele Wege offen: string tangaOI; *1 '2 £.n Stringnvt string tanga02("Stringtanga"); *2 S dem Inhalt .smngtinga’ char ctanga[) string string string string string *6 Kopecrt vorn String tanga03 .ib dem dritten ZcXbc-n 5cch& Zechen In tanga06 (-Siring) *5 Kopiert den String tanga03 jb dem dritten Zeichen "Ze-Stringtanga”; *3 tangaOS(ctanga); tanga04(tanga02) tanga05(tangaOl» tanga06(tangaOI, tanga07(10» 'x’> 3 3); •! 3, 6) *7 "3 Hierbei wird ein String Aut dem c string ctanga erzeugt. wird angelegt. *4 Dabei wild ein String ilk einem bereift eKßtierrndrn String erzeugt. nach tanga05 {«Stringt.in^i). 476 Keoltel vICUZCHR
|H»nterfrundmfo) Für den Fall, dass du zu lange in der Sprache C programmiert hast, solltest du auf jeden Fall wissen, dass die Klasse string nicht unbedingt das String-Endezeichen ’\0‘ speichert’ Auch der Zuweisungsoperator (operator= ()) der Klasse string wurde so überladen, dass du einem String auf mehrere Arten weitere Zeichen zuweisen kannst. Hierzu einige Beispiele, die dir zeigen, wie du deinem String etwas zuweisen kannst: string tangaOI, tangaOZ, tanga03, tangaOA, tanga05, char ctanga[) "Ze-Stringtanga”; *1 Ent werden ein pAar leere Strings erzeugt. *2 Hier ßibt’i eine *3 Natuflr<ti kann aixh cm stringeinem string jugcwicsen werden, cangaOl - tanga02 = canga03 cangaQA tanga05 einfache Stnng-Zuwei$ung. "Stringcanga**; *2 tangaC 1; ’3 — ctanga; h4 fx»; *5 tanga06 "Mohr Stringtangas"; "6 ”5 Etn einfache < char Zeichen geht natürlich auch. 3 *4 Die Zuweisung eine* GStfings Ift auch möglich. '6 Eine Mehifachnjweiuing geht so: Hier bekommen tanga05 und tanga06 beide der String .Mehr StringtangAS’ xugewieten. Standjrdfc lAllf Btrhr.g 477
tAbUc«] Wie schon beim Datentyp char, der für gewisse Zeichensätze zu .klein" ist und für den es für solche Fälle wchar_t gibt, gibt es mit wstring auch für string eine Spezialisierung auf „größere" Zeichensätze. Mit dem neuen C++11 -Standard gibt es aber nun auch bessere und leichter portierbare char- und string-Typen für Internationales: char!6_t und char32_t, sowie ul6strlng und u32scring Zum Beispiel: using std::ul6string; ulöstring s!6 = ; LHirtteqjrundinfp] Auch wenn es dir bereits bekannt sein sollte: Das erste Zeichen im String hat immer den Index 0, das zweite den Index 1 usw.ü!
Um auf einzelne Zeichen zu rück zugreifen, verwendest du Ja bekanntlich den Indexoperator ( J und die Elementfunktion at ( ). Der Vorteil von at ( ) liegt darin, dass diese Funktion im Fall einer unerlaubten Positionsangabe die Ausnahme out_of_range wirft. Hierzu einige klassische Zugriffsmöglichkeiten auf einzelne Zeichen in einem string: string tangaOl("String und Tanga"); char stchar - tangaOl[7]; tangaOl(12| - 'o'; *1 char kstref = tangaOl.at(15); *2 scref • 'o'; *2 try { cout « tangaOl.at(20) « endl; *3 > catchf outofrange e ) { *3 W cerr « ”11! out_of_range 11!'• « endl; . cerr « c.what() « endl; / k fc2 Sa geht ei natürlich auch. Zunächst verweil! eine Referenz Auf rin Zeichen, wobei wir hier du* f Irment- funktion at.( ) verwenden, und dann wird dlcvcs F Zeichen in» String über die Referenz geändert. ”1 Einem einzelnen Zeichen in string wird ein Chat Zeichen zugewiesen. Im Rall einer Bereichsuberschreitung mit at ( ) , die wir hier Absichtlich provozieren, bv»fd die Aufnahme out^of^ränge geworfen Dagegen kannst du d«e Funktion nut einer Ausnahmebelundlung impfen Di« StandarUkJass« Strang 479
Für die Ermittlung und Änderung der Lange gibt’s natürlich auch eine Handvoll Elcmcntfunktiancn wie auch für die Ermittlung der größtmöglichen Länge eines Textes. Einige dieser Elementfunktionen bei ihrem Dienst: string tangaOl; if ( tangaOl,empty() ) { *1 tangaOl •'String und Tanga"; *1 ) cout « tangaOl4si*e() « endl; *2 cout « tangaOl.length() « endl; *2 cout << tangaOl<max_$ixeO « endl; *3 cout « tangaOl-capaeityO « endl; *4 tangaOl.reservef tangaOl,aize()*2 ); *5 tangaOl-resize(6); *6 "2 Oamit wird die Anzahl der Zeichen im String zutikkgegebeo. ’1 Hier wird überprüft, ab der String tangaO 1 leer i&t (true), in dem fall .iuch njtnffl. Und daher wird die Zuweisung Im if-Block auch aiwgduhn. ’3 Die&e ElfmentFunktion gibl dw Anzahl der Zeichen zurück, die in den String passen *4 Gihl d t Anzahl der Zeichen zurück, d.r im String gespeichert werden können ehe erneut Speicherplatz reserviert werden muss *5 Ändert di» Kapazität dn Swings auf den vorgegebenen Wert. tangaOl.resize(10t ’-f); *7 tangaOl.cle*r(>i *8 *6Ändertd> -Länged«Swnp Wird em Ide nerer Wert clngegcben ah der aktuelle Wert wn Capacity ( ) . wird der String auf try ( den angegebneren Wen gekürzt. Im Beispiel wird der Inhalt tangaOl. reservef tangaOl .max_size() ♦ 1 ); ’9 von tangaOl aut "String1’beschränkt catcht length_error ie ) { *9 cout « "length_error abgefangen\n"; cout « e«what() « endl; *7 Wre eben zuvor. nur dass der Rest mit dem char- Zeichen gefülH wird. welches als zweiter Paiameter angegeben wurde, wenn die Größe langer ist als der vorhandene Text im String. Hier wird aus tangaOl gleich "String-------------------------" “9 Zur Demonstration versuchen wir die Kapaz ldl des Strings auf max^slz e ( ) «1 zu erhöhen um so dir Ausnahme length error zu bcschwöten und abzufangen. "8 Löscht alle Zeichen »m String, und setzt die Länge <Ä iz« ( )) auf 0 zurück. Mit C++11 gibt cs jetzt auch die Möglichkeit, den Inhalt eines Strings nicht zu interpretieren. Im Grunde bedeutet das zunächst nichts arideres, als dass du den Backslash .V direkt verwenden kannst und dieser nicht mehr als Fluchtsequenz verwendet wird. Das ist besonders hilfreich bei den regulären Ausdrücken (ebenfalls neu dabei mit Ch11) oder der Angabe von Dateipfaden unter Windows. Einen solchen rohen String (Raw String-Literale) kannst du In 0*11 mit R" (roher String" ) (genau hingucken beim Anführungs- zeichen und Klammern srtzcn(!)) definieren: *1 Gibl jtttt EMS&hlkh Vl urtd \l Jus string roherString = string(R" (Raw String: \n un<J interpretiert d.«eZeichen nkhl ab COUt « roherString << endl; *1 Newline-hrw. Tabufatorem-Zeichenl 480 ICpLttl vlCKZCHM
Noch mehr Unterwäsche Okay, ich denke, das Thema ist nicht allzu komplex, und du kannst es erst mal etwas lockerer angehen. Daher werden wir hier auch etwas aufgabenlasiiger vergehen. Viel Spaß dabei! [tinUch* Mal sehen, wie gut du den Umgang mit einzelnen Zeichen beherrschst. Manipulier im String "Ein Text mit viel e*1 den Buchstaben ’e' so, da» du alle ’e1 durch ein ’x' ersetzt! string soelnsch("Eln Text mit viel e"); •1 Hie» durchlaufen wir in einer Schleife umllxhe Zeichen vom Stnng soeinsch Der String hat eine Lange von size() Zeichen, was auch die Abbruchbedingung der Schleife darstelH for( size_t i 0; 1 < soeinsch.size(); 1++ > { *1 if( soeinsch.at(i) == ’e' ) { *2 soeinsch.at(i) ’x’j *3 > ’• . > •2 Wiz uberpnjfen4 ob das aktuelle 2e»chen rin 1 e r Ist. und ... *3 . Indern den BuchsMben ,e im Fall der zuLrettenden Fällt* in den fiucbitabcn r X 1 um, w> dasi der Sinng >im Ende "Ein Txxt mit vixl x" lautet Wow! Super gemacht. Schrödinger’ ttiäfACht Aulgab*| Wahr ja auch nicht allzu schwer, oder? Eine kleine Bonusaufgabe hatte ich noch
S^/*i. /c^, ‘V'Z-v/V'l'G. t^S string lisaC’Lisa'’); slze_t 1; for( 1=0} 1 < lisa.sizeO; llsa.at(i-l) - •*'; *2----- '1 Die einzelnen Buchstaben hochzahlen ... 1++); *1 2 . . dann .im Ende angekommen. an da$ .a' durch 4*« .e’ ersetzen. Okay. Schrödinger, du bist hier zwar ans Ziel gekommen, aber die Lösung ist einfach nur umständlich und schlecht! Du denkst da viel zu kompliziert. Sicherlich gibt es viele Wege, die ans Ziel führen, aber der hier ist definitiv der schlechteste. Mit folgendem Einzeller kommst du schneller ans Ziel: string Lisa("Lisa"); lisa(llsa.Lengch( )-l] - 1e,t|| *1 Den tetrter» fiuchiUbrn durch ein ,e4 endw. uhuu Du kannst es aber auch mithilfe der Standardbibliothek mit folgendem eleganten Einzeller probieren: *lisa.rbegin() = ’e'; Du musst jetzt nicht gleich beleidigt sein. Schrödinger! Es ist normal, wenn man anfangs etwas umständlicher denkt und den schwierigen Weg geht. Aber in C++ gibt es fast immer einen einfacheren und weniger komplexen Weg. Trotzdem bin ich stolz auf dich! Du machst dich hier insgesamt sehr gut! 482 vlCKZCHk
Und noch mehr davon lEiftfachr Au<gab*| Wie kannst du einen String bei der Definition initialisieren? Natürlich habe ich auch noch ein paar einfache Fragen an dich, die du. ohne groß nachzudcn ken. beantworten kön- nen solltest. Prima! Das ist korrekt! Dann zur nächsten einfachen Frage. Dl« St ind jr<Jh a>ll« String 483
(tivfachr Aufgabe] Was kannst du mir zur Zuweisung von Strings sagen? Wie kannst du dem Objekt string etwas zuweisen? Richtig! Hinzugefügt werden muss hierbei noch, dass bei einer Zuweisung der vor- herige Inhalt durch die neue Zelchcnkette überschrieben wird. Uni die Spelcherplatz- bcschalfüng musst du dich natürlich nicht kümmern. Es gibt selbstverständlich auch noch die Zuweisung mit dem +=-Operator zur Verkettung und die Funktion get- line ( ) bzw. den Operator » zum Einlesen von Zeichen, aber darauf gehen wir später noch extra ein. metüi«u«i] (Cfarfiche Au^ibel Im folgenden Codeausschnitt wurde ein verhängnisvoller Fehler gemacht. Finde den Fehler. Wie konntest du so etwas Einen Top-Tipp habe ich für dich zu diesem Beispiel noch. Mithilfe der C+411-ränge basierenden for-Schleife kann dir so ein Überlauf hier überhaupt nicht mehr passieren, wenn du es folgendermaßen machst: for(char c : auchdcnfchler) cout « c « endl3 von vornherein vermeiden? string suchdenfehlerC'Wo ist der Fehler?")! for( size_t 1 = 0; 1 < suchdenfehler.sizeO + 1; L++ ) { cout « suchdenfehlerli] « endl; Okay, ich glaube den ersten Teil für die Klasse string hast du ohne Probleme verstanden, und daher solltest du gleich mit dem folgenden zweiten Teil fortfahren. Zu Belohnung solltest du dir mal wieder etwas gönnen. Wie wäre es mit einer Runde WoW?! 484 C«okt«l VlC*?(Hh
Die Klasse string hat so unglaublich viel zu bieten. In diesem Abschnitt erfährst du mehr darüber, wie du Objekte der Klasse String nachträglich verarbeiten kannst. Auch für die Suche stehen interessante Funk- tionen zur Verfügung. Manchmal kann es sein, dass du die Klamottenkiste brauchst und einen String in einen C-String konvertieren musst. Für so einen Fall gibt es natürlich auch etwas, genauso wie Element- funktionen für das Einfugen. Anhängen, Löschen oder Ersetzen von Text. Hierzu wieder entsprechende Element- funktionen in Aktion: M Mit der Elemenitunktion copy ( ) k.innU du vom aktuellen String eine* bestimmte Anzahl von Wehen (rwedrs Argument) ab c^ncr bestimmten Position («jftttes Argument) in einen C-String (erstes Argument) kopieren Das String- Ende Zeichen wird nicht mit kopiert! "2 Mit dat a () e<hälut du die Anfangsadresse (nur lesbar) dev gespeicherten Strir^s zurück, um diese z Ö für die alte Nqst.ilgiHunktipn Strncpy ( ) zu verwenden 4 Mit appcnd() kannst du einen Text (hier .schürz“) string tanga("String und Tanga”); char ctanga[20); tanga,copy(ctanga, slzeof(ctanga)» 0); *1 const char *ctptr tanga,data(); *2 strncpyf ctanga» ctptr, sizeof(ctanga) ); "2 •3 insert() fugt ab der PömIidh 6 in tanga die Textfolgc Höschen- ein so dass der Inhalt von tanga nun .svm^höschcn und Tanga" statt,String und Tanga" lautet. ans Ende des Stringi hangen womit tanga fetzt .Stringboschen und TangaKhurz" lautet "7 So geht es auch, wobei hierbei alles hinter <fer Position 14 im String tanga gelöscht wird. In unserem F.ill dai Wort .Tijngawhurz" womit m tanga nur n<Kh die Textfotge .$U«ngh<H<hen" enthalten »st. tanga.Insert(6, "höschen”); ’3 tanga,appcnd("schürz”); *4 string hose(”elne Schande”); *5 tanga,insert(15 hose, 5» 3); *5 tanga.erase(14p 7); '6 tanga,crase(14); *7 string tangaOZ("String und Tanga"); string ohne("ohne"); string hoeschen("ein Höschen”); ’S Natürlich geht das mit insert ( ) weh etwas komplexer Hier fügen wir ab der Position 11 von tanga vom string höSC ab der Position 5 insgesamt drei ZeKhen ein um Bespiel Sch ) womit in tanga nun ,S1ringhö$chen Schund Tanga“ steht "6 Mit etase ( ) können Sie eine bestimm- te Anzahl von Zeichen .ws einem String löschen Hier entfernen wir aus tanga ab der Position 14 genau sieben Zeichen In unserem Fall wird h^er das Wort .Schund" komplett entfernt, womit nur noch .String- hOschen Tangawhurz“ in tanga steht. Da* St-jnij jrjb * t i r.g 485
tangaO2,replace(7 , 3» ohne); *8 tanga02>replace( 7, hoeschen, 4, hoeschen*size()-4); *9 ’B A\it replace( ) k*winit du einen Teilstring durch einen .indrrm Stnnfl überschreiben n diesem fietspie werden «m Sinng tangaOZ ab Position 7 drei Zeichen durch den String ohne ersetzt in diesem Beispiel wird somit aus .String und Tanga- der Text .String ohne Tanga- *9 Natürlich gib*, cs ziKh hierzu noch e^ne komplexere Alternative Hier ersetzen wir im tanga02 ab Position 7 vier Zeichen durch den String hocschcn ab Position 4 md hoc sehen. s ize()-4 Zeichen (also .Hbschcn"). Somit wird aus .Stnng ohne Tang.r m tanga02 <kr SVing .string Höschen Tanga" (Ablage] '4^ Ausgaben mit cout kannst du dir ja mittlerweile selber ein- bauen, wenn du diese Beispiele in der Praxis sehen willst. Eine weitere Funktion, die ich nicht unerwähnt lassen will, ist substr (). mit der du vom aktu- ellen String einen String von Position x mit y Zeichen extrahieren und zurückgeben kannst. Folgendermaßen kannst du bspw. aus quellstr einen Teilstring ab der Position 5 mit exakt zehn Zeichen in zielstr speichern: zielstr=quellstr-substr(5, 10); Such! Auf der Suche nach (Teil-)Strings oder einzelnen Zeichen stehen dir auch Funk- tionen zur Verfügung, deren Anwendung recht einfach Ist. 488 vlceztMK
•i Mit find ( > kannst du Im aktuellen String nach dem ersten Vorkommen e*nes Suings suchen Zurückgegeben wird die Position des Vorkommens. Im Beispiel Suchen wir im String essen nach dem String .Gockel' und speichern diese Povtinn in p 1 ‘2 Diese gefundene Position verwenden wir auch gleich, um den String .Gockel- in essen mit replacef) durch den Stnng .Schndzel* zu ersetzen Such, such ... string essen("Ich liebe Gockel mit Pofimes*1 >♦ size_t posl = essen.find("Gockel"); *1 essen.replacc( posl, sizeof("Gockel")-ls "Schnitzel" ); *2 cout « essen.find(’e*) « endl; *3 cout « essen.rfind("e’} « endl; *4 size_t pl - essen.find_first_of(" \n\t”, 0)T p2«0; "5 whilc(pl I- string::npos ) { *6 cout « essen.subscr(p2f pl-p2) « endl; *7 p2-pl+l; *8 pl essen♦find_flrst_of(" \n\t\ p2); ’9 > cout « essen.substr(p2i essen.sizc()-p2) « endl; *10 "7 Hiermit <?*trj.h»cren wir mit Substr() die einzelnen Wörter aus dem String essen Im Beispiel geben wird diese nur auf dem Bildschirm aus. •$ Damit wir beim njtfhytcn Durchlauf nicht wieder dasselbe Ergebnis zuruckbckommcn und um in einer Entfiosschlcdc verlieren, setzen wir p2 um ein Zeichen nach vorn. *3 Gib: die Position des ersten Auftretens vorn Buch staben ,e zurück *4 Mit rfind() hingegen wird das letzte Auftreten, hier der Buch- stabe .e*, gefunden. rfind( ) lasst sich natürlich auch mit Strings verwenden. *5 Mit find—first_of () suchen wir im aktuellen String, angefangen bei Position 0, nach dem ersten Vorkommen einet der angegebenen Zeichen, hier nach Whitcspace, Newline- und Tabulatorenzeichen. Die Position speichern wi< in pl "6 Wir gehen In die Schleife und prüfen, oh die Position von pl glexh npos ist Öles wird rum lieh zurückgegeben, wenn nichts gefunden wurde *9 Jetzt suchen wir crneul ab der Position p2 nach einem Leerzeichen. Newline- oder TabulatorenzeRhen und speichern den Rückgabewert in pl Wenn bei der while-Schleile noch nicht npos ermittelt wurde, gehl es zum nächsten Wort. *10 Das letzte Wort .schneiden' wir nochmals c*tfa raus. Pie Standarjit string 4B7
(Ninterfrundin* *o| Neben den verschiedenen find_flrst_of-Versionen gibt es natür- lich auch hier noch mit find_last_of das Gegenstück und mit flnd_flrst_of_not bzw. flnd_last_of_not nochmals ein paar Gegenstücke zum Gegenstück. jberladene Operatoren 1111 Ebenfalls wurden viele Operatoren sinngemäß für String überladen. So kannst du bspw. alle Vergleichsoperatoren auf zwei String Objekte anwenden. Natürlich wurden auch die Ein- und Ausgabeoperatorcn >> und « passend überladen. Aber schau am besten einfach selbst: string name, nnamc, voll? cout « "Name : ; *1 ein » name; "1 cout « "Nachname : ; ‘1 ein » nnatne; *1 if( name nnaoe ) { '2 cerr « } eise if( name > nnante ) cout > eise { cout } voll = name+“nnamet ‘5 name+«" (Mustername) *'; ‘6 cout « voll « endl; cout « name « endl; "Vorname und Nachname gleich???\n”t < *3 "Nachname ist lexikografisch größeren"; "Vorname ist lexikografisch klciner\n"; « « name + " " + nnamei •1 Auch wenn du es miulefwerlc als selbstverständlich ansiehst; Auch die Operatoren » und « wurden für die Stindaidein- und Ausgabe überladen. •4 ’5 Hier wird der String unafiQe direkt an den String name angchAngt *6 So itt es nalürlkh auch möglich mit dem Anhängen. 4 2 Damit kannst du rwei Strings auf Gleichheit hin überprüfen. Wahr- heitsgemäß »st das true onwistrn eben false Natürlich kannst du auch das Gegenstück != dazu verwenden. *S Hier checken wir einfach, ab der linke $thnß (hier name? Ir:okograf*4ch großer ist als der rechte String [hier nnamei Auch hier gibt’s bd Wahrheit true und bei Falschheit false iurück. Sinngemäß kannst du hierbei auch die Operatoren «. <=>= einseuen. ’4 Mit dem +-Operdtcw kannst du Strings miteinander verketten saneinanderhängeni Neben String kannst du hier auch char* und char verwenden Allerdings muss mindestens Hner der Operanden (also Strings) ein Objekt der Klasse String sein. Im String voll steht somit dann det ein- gegebene Name und Nachname mit einem Leerzeichen getrennt. 488 Capitel VXCeiCMH
Ja, es ist tatsächlich so, dass sich » nur für einzelne Wörter eignet. Zum Einlesen einer ganzen Zeile empfehle ich dir, auf getline ( ) zurückzugreifen. Hier also die Funktion um eine ganze Zeile in einzulesen: string zeilel, zeile2; cout « "Zeile 1 bitte s gecline(cin, zeilel); *1 cout « "Zeile 2 bitte : getllne(cin> zeile2, lel); *2 *1 Damit wird dir komplette Zeile b-5 zum nächsten Newiinc- 2c*cticfi «angelegen. Den Sve-im ein musst du hiev als zweiten Parameter angeben, und wo die Eingabe gespeichert wird, findest du im zweiten Parameter *2 Entspricht dem im Grunde, nur wird hier entweder b<s zum nächsten Buchstaben ' C 1 oder bis rum nächsten Newhne-Zeichen emgelcsen.
Ich kann's nicht mehr hören: Strings In diesem Abschnitt wollen wir uns einigen .komplexeren' Beispielen widmen, um einzelne Strings zu manipulieren. Du hast ja im Büro zuvor einige Elementfunktio- nen dazu kennengelernt. Mal sehen, was du drauf hast. (tinfichr Auffibel Häng die beiden Strings zu einem String zusammen. In papalapap soll am Ende .Sinnloser Text' enthalten sein. string papal;sp;»|i("Siiinlc.»;er"} ; string textf"Text"); *1 _ papalapap.append(M ; papalapap.append(text); -1 Fwpapalapap und text zu clnrm String zusammen (mit Leerzeichen dazwischen) Prima gemacht, Schrödinger. Alternativ könntest du hier auch noch den überladenen += oder + Operaior dafür verwenden. Zum Beispiel mit folgendem Einzeller: papalapap + ” " ♦ text; f Fuge hier zwischen .Text’ und .Sinn’ d*c Zeichenfolge .nur cl CSchrt-itrtge Aufgab*] Okay, wollen wir mal sehen, wie es mit dem Einfugen und Ersetzen aussieht. Folgende Strings sind vorgegeben: Ißtrlng string string string - Füge hief zwischen .ton- und .gut" den Tellstfing .sehr von blabla ein. "3 Eiseue hier den Teilwring .schlechter" duich den TeilsUing .toller' den blabla textmitsinnC"Text Sinn”); ’ ichkanns("Ich bin gut”); *2 ichkannsnichc(”Ich bin ein schlechter Mensch”); blabla("Ein sehr toller Film"); "4 M Ersetze hier den Tellstrmg .toller" durch den String .schlechter*. 490 hpittl vICtZCHH
*1 Hur fuge ich ab Position 5 den String «mit* zwischen .Text' und .Sinn' ein, womit in textmiXsinn jetzt die Zcicbcnkcttc .Text mit Sinn’ enthalten ist Das Programm bei der Ausführung Nachher: Schroedlnger $ nir« Text Sinn Ich bin gut Ich bin rin schlechter Mensch ttn (sehr jtolTer] FiU Text ait Strai Ich bin hrhrlgut Ich bin ein toller]Mensch tin sehr schlechte” Hl« ein wenig höher legen cextmitsinn*inserc(5, "mit M); *1 ichkanns»insert(8. blabla» 4» 5 ); ’2 lchkannsnicht>rcplacc(12, lOt blabla» 9, 6 }; ‘3 blabla.replace(9» 6, "‘schlechter”); *4 ’2 An der Position 8 vom String ichkanns fü^e ich hier vom $inng blabla ab der Position 4 exakt fünf Zeichen cm Somd w.rd aus Ickkanns mit -ich bm gut" eben Jch b-n sehr gut" Das «sehr- habe ich ja von blabla eingefügt. und rrpUc« Schroedlnger S clcor Schroedlnger $ ./neotn Vorherr *3 Ojnut cnetze ich tn ichkannsnicht ab der Position 12 genau zehn Zeichen (was hier der Teilst ring «schlechter- ist) und lüge dafür e nen Teilstring aus blabla ein. Dieses ASal verwende ich dort .ib der Position 9 exjJkt sechs Zeichen (in diesem Fall dann Jolkr’) Öam«t stehl m ichkannsnicht dann „ich bin ein toller Mensch" 1 Und hier ersetze ich in blabla ab der Position 9 genau sechs Zeichen durch die Stnng-Konstante Rechter", womit in blabla nun die Zeichen- folge -Em sehr schlechter Firm" steht Okay, dann wollen wir die Messlatte mal ßthwieripe Avlgibe) Schreib ein Programm, mit dem du eine ganze Zeile in einen String mcinstring einlesen kannst. Danach erstellst du eine Abfrage, nach der du im String meinst ring suchen willst, und dann noch eine Abfrage, durch die du den eventuell gefun- denen Teilstring ersetzen willst. Zu guter Letzt ersetzt du den Teilstring dann auch. Also ein einfaches Suchen und Ersetzen halt. Natürlich solltest du auch reagieren, wenn die Suche erfolglos war. Pie StindjrQh1*131 Strfcng 481
Komm schon, ich weiß, dass du das hinbekommst. Du brauchst doch lediglich drei Objekte der Klasse string. einmal den einzulesenden Text, dann den Suchstring und schließlich den String zum Ersetzen. Dann suchst du in deinem Text nach dem Suchst ring, und im Fall einer erfolgreichen Suche (l =31ring: : npos) ersetzt du mit dem String, den du für das Ersetzen eingegeben hast. string meinstring, suchstring, replacestring; cout « "Bitte gib ein paar Wörter ein: 11; ’1 getlincfcin, tacinstring); 1 cout « "Wonach willst du getllnelcin, Suchstring); cout « "find getllnefcin, sizc_t posl »cinstring.find(suchstrlng); *4 if( posl == string::npos ) { *5 cerr « suchstring « " nicht enthalten 111\n"; > eise { meinstring.rcplacc( cout « meinstring « endl; ’2 Hut lesen wir den Suchstringeln ... *1 Hier geben wir den Text über getline() ein. suchen: *2 •2 — womit ersetzen : ”; *3 replacestring); *3 *6 Wurde der suchst ring in meinst ring gefunden, ersetzen wu diesen mit replacestring posl, suchstring.slze(}, *6 replacestring ); '3 ... und hier den Stnng für d r Ersetzung ‘4 Mit der Eleinentlunktior find ( ) suchen wir nach den' SUchstring in 'S Wenn nichts gründen wurde. w»rd string: :npos zurütieegeben 482 vic*;(kw
Das Programm bei der Ausführung Bitte gib ein paar Wörter ei Wonach willst du suchen: Und wcxwxt ersetzen : _ __ Hein<£ömputerkennt Else nicht! Schroedingcr S ; Suchen & ___________________ chnerJkennt Else nicht! hn Ich bin der Meinung Das war spitze! Wirklich, Schrödinger, das hast du wirklich sehr schon gelöst. Ich bin stolz auf dich. Du bist mein bester Schüler im Augenblick! 493
Allas sauber dank „Schwarzer Zwerg* Prima, Schrödinger* Jetzt sind wir auch hier ans Ende gekommen. Du weißt jetzt, wie du die Klasse String in der Praxis vielseitig einsetzen kannst. Damit hast du eine sehr leistungsstarke und zuverlässige Waffe im Umgang mit Text kennengelernt. tz«u*n Für trockenere und tiefere Referenzdetails empfehle ich dir, immer mal einen Blick in die Dokumentation deines Compilers zu werfen oder in eine der vielen tollen Referenzen im Web Meine Empfehlung hierbei ist: http://www.cplu3phi3.cofn/ Du kannst dich jetzt gemütlich zurücklehnen, weil ich jetzt noch ein paar Fragen an dich habe, die du ohne deinen Rechner beantworten kannst. C* "I w {Einfache Autfgibel D«e gleich folgende Zeile wirft die Ausnahme out_of_range, wenn weniger als 30 Zeichen in eintext enthalten sind. Was könnte die Ursache sein? •1 Hi« wiid die Auinahmv QUt_ ofrange geworfen Was tot hier die Ursache? eintext.replace(30, suchstring.sizef), replacestring); *1 SÄ'? Zi./j Ä/^4***' 5#. ^*4^4^ [Einfache Au^Jibcl Sehr gut gelöst! Zugegeben, das Beispiel Ist ziemlicher Murks, aber es soll dir verdeut- lichen, dass man sich auch hei den Elementfunktionen seine Gedanken zum Einfügen, Löschen oder Ersetzen machen sollte! Was geben die Suchfunktionen zurück, wenn nichts gefunden werden konnte und sie am Ende des Strings angekommen sind? Z/äS string::npos Ja, das ist wirklich einfach! Hinzufügen muss ich noch, dass es sich hierbei um eine statische konstante Variable vom Typ size_t (ein unsigned Intcger-Typ) handelt, mit dein Wert -1 definiert. (Eiafache AulgAbtl Wo liegt der Unterschied beim Einlesen von Strings mit » und getlincO? IfitlöhnüAgi'Ltisung] Du bist guuuut! 4S4 Cwpktti vlCAZtM
—FÜNFZEHN— Der Umgang mit Streams | (Jlim und Dateien ohne Isolierkabel verwenden Der Umgang mit Dateien und der Ein- und Ausgabe wird In C++ Uber ein spezielles und erweiterbares Streamkonzept Implementiert. In diesem Kapitel lernt Schrödinger das Prinzip eines solchen Stroms (der hier nicht gelb ist) näher kennen.
Alles, was in C++ rein- oder rausgeht, wird auf einer sehr liefen Ebene geregelt. Genauer gesagt, handelt es sich hierbei um einen Strom (engl. Stream) einzelner Bytes- Natürlich hantierst du dabei jetzt nicht am Stromkabel ohne Isolation herum, weil t*rf riHMfcnniti««] du sonst einen Schlag bekommst. Die einzelnen Ströme erhalten ihre tiefere Bedeutung erst auf einer anderen Ebene. Okay, spre- chen wir nicht geschwollen drum herum, das ganze Prinzip basiert natürlich auf einer Hierarchie von Klassen-Templates Auf dem Thron regiert die Klasse ios_base. sic ist also sozu- sagen die Siroinleitung ohne Isolierung. Von ios_base wer den alle anderen Ströme abgeleitet. Die Bezeichnung Ströme oder Streams (wir werden uns auf den Begriff Strom einigen) kannst du dir bildlich wie einen Strom vorstellen. Und zwar als Objekt, das Daten bspw. auf einen Bildschirm, in eine Datei oder von der Tastatur zum Programm ..fließen' lässt. In der folgenden Abbildung siehst du eine einfache Darstellung der Klassenhierarchie für die Ein- und Ausgabe, die allerdings schon wieder eine Spezialisierung für einen Basisdatenryp dar- stellt. Abgesehen von ios_base haben nämlich die eigentli- chen Klassen Templates alle das Won basic_ vor Ihrem Klas- sennamen stehen. 496 roxrzcHW
Vereinfacht* Darstellung der Klaswihierafchte iSirrn^rtrtfln* typedef bÄSic_ostream<chür> ostreaa; li'CtreiiFw *1 Ein Spezialisierung für den Datentyp char am dem Kl.iiscnJemplatr baslc_OStream Natürlich Ujwn sich die einzelnen Klaswn-TempUt« aixh für erweiterte ZelchenyiWe verwenden (wie dies bspw mit wchar_t der Fall «st». so /jtredn^i Im Header <iostream> findest du dann z. B. die Standardslröme mit einem Objekt der Klasse ostream definiert: namespace { extern o«tre«a cout; extern ostrean cerr; extern ostreara clog; > Und da haben wir schon eine perfekte Oberleitung zum Strom OS tream gebaut, Endlich kann ich das Geheimnis vom Operator « lüften: Du weißt ja bereits, dass ein Strom zunächst nichts anderes ist als eine unleserliche Folge von Bytes. Em eine Spezialisierung des Operators « mit der Klasse ostream für die eingebauten Datentypen macht es möglich, dass du auch das zu lesen bekommst, was das (normale) menschli- che Gehirn verarbeiten kann (also dass kleine ASCII-Zeichen ausgegeben werden). U»g«ng «>t Str«««». und 6«t«t«n 497
Hier ein Ausschnitt aus dem Header <ostream> vom «-Operator: ostreami operator«(const char*); ostreamt operator« (char); ostreami operator«(int); ostreami operator«(float); Erst dadurch, dass als Rürkgabciyp eine Referent verwendet wird, ist die Reihenschaltung der « Operatoren möglich. Folgendes meinte ich damit: int ich - 123; cout « "ich “ " « 123 « endl; IZeUel) Dir sollte natürlich klar sein, dass es sich bei den dargestellten operator«- Funktionen uni Mitglieder einer um- gebenen Klasse (ostream) handelt und nicht um eine .freie" Funktion. lAbl-IC«) Dass du auch eigene Klassen mit « überladen kannst, habe ich dir ja bereits gezeigt Ich hoffe, du hast es nicht vergessen?? Neben den mehrfach überladenen « Operatoren stehen dir in <ostream> mit put () und write ( ) auch noch Elementfunktionen zur Verfü- gung. welche die Daten unformatiert ausgeben. Hier diese rohen Sachen bei der Arbeit: string eiC'rohes Ei"); IZtUdl Wesentlich eleganter und einfacher kannst du dir die for-$chleifc im Beispiel mit der neuen C++11 -ranged-for-Schlelfe wie folgt machen: for( char c : ei ) cout.put(c); j *1 Hier gibst du P unformatiert '•irurlne Zechen auf dem Bild- schirm aus for( slse t 1 0; 1 < ei.sizeO; i**) { cout.put(ei.at(i)); *1 } cout « 10 « endl; *2 cout.put(10); H Newline *2 char cstr[| "Rohe Daten1'; cout.write(cstr, 5); '3 cout.put(10); •2 Hier dc Vergleich jFaAnvrhr-n formatiert und unformatiert Mi! « wird tatsächlich der formatierte integer 10 ausgegeben Mir put () hingegen wird nur <k?r rohe Wert für 10 ausgegeben. Und wenn du hier einen Blick dui die ASCII-Tabefle wirfst, sieht der rohe Wert für das Newline-Zeichen. '3 Mit write ( ) ist et auch möglich, eine bestimmte Anzahl von unformatierten Zeichen .luwugeben hi dem Fall ct es natürlich auch gefährlich, weil wir die Anzah’ der Zeichen, die wir ausgeben, nicht überprüfen. 488 FoMfZtMh
Mehr formatierter Output bitte Natürlich gibt es auch Optionen, mit denen du die Ausgabe etwas ordentlicher gestalten kannst und nicht einfach so rausknallen musst. Daher will ich dir ein paar Möglichkeiten zeigen, wie du dl? Ausgabe formatieren kannst. Für die Steuerung der Weite von Fließkommazahlen und Füllzeichen werden dir auch einige Funktionen zur Verfügung gestellt. Aber sieh es dir am besten selbst an ________:i DjfluL^gtrt du dac Füllweite dckAumabc aid zehn Zeichen_ Sprich, wenn deine Zahl oder dein String sechs Zeichen enthalt, werden dir restlichen vny Zeichen mit lerwichcn Ju.gHullt Ruht du dlrsr Funktion ohne Argumente auf, wird der aktiiclle Standardwert der Feldbreite zurüdcgcgcbcn ____*2&UU der l; urxuichcn_____ kinnst.du.abcf auch nut f ill () em andern Fülltet- Int pln • 123456; cout.widthf10); *1 cout.fill(***); *2 cout « pln « cout- « pln « endl endl *3 Hrer erfolgt die Ausgabe jetrt mit einer Wrih von rehn Ze rhen unc* dV Rest wird mit * ____aufcef U JLalifl_ ****123456 ihMjngättfi-. Verendest du f ill ( ) ohne- cm Argument. wird das Fuilr^rLwiedcr auf den Standard (Leerzeichen) zurückRe&etzt. 3 4 [ *5 Hie«n«t will «ch __du zeigen, dass im Fal« • zu kleinen Weltenangabe nicht der eigenUicbc Wert geküret wird, _ ~Auch wenn du hier eine dn^ibst. wird trotodem def komplette Weit ausgegeben und nicht abgeschnitten. cout.wldth(4); cout « pln « •5 endl; S ______*4 An diemv Stelle wird die Fullwrite von____ zehn Zeichen nicht mehr vrrwervdet Daher kannst du Khluisfol^ern-daMcIie Angat L«ün widthO- _____nur föf die nächste Ausgabe gültig btelbt und_ dann wieder auf 0 zunxkgesetzt wird Oas Füllzeichen hliygefi bteK bestehen, wenn du es mit ~ f'ill ( ) gi-dndurt hast. cout.precislon(); "6 Int Standardwert cout « Standardwert « endl; double dval 1234.9234; cout « dval « endl; •6 cout.prccisfon(7); *7 cout « dval << endl; *7 ------------*6 Einu tpauull«! W*iUinai^|Abu------ gibt es natürlich auch für die Fliefikomm^zahlen mit precislon( ). Mje< ermitteln wrge-r.Kk- den Standardwert dieser Funktion In diesrm Beispiel Ht der voreingestellte Standirdwert 6. was d e_ anschllrtwde A cout*preclsion(4); SS cout « dval « endl; f8 ’S Hier wird dk Weite ‘ ! > Ganzzahfw wert reduziert, und dahe^ tindet hier auch ejne .lutornatische Rundung au? 1235 statt. 47 Hier ändern w r die Weite auf sieben Ziffern, wc-mit die anscMiefiende Ausgabe 1234.923 .'.11 t<-t Der U»9«ng a>t und Dateien 499
‘1 Hier sichern wir den S-Und.ird- zutf .uid der Flagge*! *3 Damit wwd der aktuelle Flaggen - zustand zurück* gegeben und in standardFlaggen gespeichert. ^Vlr^hwenken die Flagge Mit hl von sogenannten Flaggen (Flags. Fähnchen) und speziellen Funktionen, um diese zu schwenken, kannst du auch die Ausgabe steuern. Intern ist hierfür der Daten- typ ios:: fmtflags (hier eine Spezialisierung von ios_base:: fmtflags ifnntgtayp char) definiert. in Beispiel, wie du Fähnchen für die Ausgabe schwenken kannst: ios::fmtflags standardFlaggen; *1 ios::fmtflags neueFlaggen - ios::hex | ios::uppercase; *2 standardFlaggen cout.flags( ); *3 cout.flags(neueFlaggen); ’4 cout « 255 « endl; '4 cout.unsetf( ios::uppercase ); *5 cout « 255 « endlj ’5 cout.setf( los::showbase ); cout « 255 « endl; '6 cout.setf(standardFlaggen); cout « 255 « endl; '7 6 7 ’4 Mit f lagst) kennet du aber zuch gleichzeitig neue Flaggen schwenken, wenn du dies* als Argument verwendest, wie wir dies hier mit neueFlagge gemacht haben. Alternativ konntest du hierfür auch die Element- Funktion SCtf ( ) verwenden. Die Ausgabe lautet hier .FF" (die hexadezimale Groß- Schreibung für 255>. $ Hie! deaktivieren wir mit unsetf () <he Flagge ios: :uppercase weshalb die nächste Ausgabe des hexadezimalen Wertes 255 nun kleingeschrieben .H* lautet. AJternatrv könntest du Stattdessen auch cout.setf(0,ios::uppercase); schreiben. ‘2 Dies verwenden wir ak neue Formaternsteb lung für die Ausgabe Mit los:: hex werden Zahlen In hexadezimaler Schreibweise ausgegeben, und mit ios: :uppercase werden GroÄ- statt Kleinbuchstaben bei den Zahlen verwendet (bspw FF statt ff für den hexadezimalen Wert 255k Hier suchst du auch, dass mehrere solcher Flaggen mit dem bit- weisen ODER-Operator zu einer Maske verknüpft werden können. “6 Hier aktivieren wir mrt setf () zusätzlich die Flagge los::showbase um die 6av$ mit auszugeben im Ocrspid wäre dies m der hexa- dezimalen Schreibweise Ox AKo lautet die Ausgabe Ox£f. •7 Hier stdlcn wir wieder mit 8£tf ( ) und den Flaggen standardFlaggen den Ursprungszusland her. den du zu Beginn darin gesichert hast. 500 fomfzcmw
Hier deine richtigen Fähnchen! Musst halt noch ios voranstellen: i Sinn und Zweck showbasc Bash mit anzeigen showpos bei positiven Zahlen ♦ voranslellen uppcrcasc Großschreibung (X. E statt x, e oder A, 0, C. D, E, F statt a, b. c ct e. 0 showpoint nachfolgende Nullen bei Gleitkommazahlen Ausgeber unitbuf Puffer leeren nach jeder Ausgabefunkbon atdlo Puffer leeren nach jedem Trennzeichen Diverse Forrnate»nstellungen für die Ausgabe Für die Ausgabe von Ganzzahlen findest du folgende vordefinierten Flaggen in ios: ibasefield: । Sinn und Zweck I oct Ausgabe im oktalen Format dec Ausgabe im dezimalen Format hex Ausgabe im hexadezimalen Format Autgabr eiw bestimmten 04 sh Hier noch einige Flaggen für Gleitpunktzahlen, welche, ausgenommen showpoint, in ios: :floatfield definiert sind: I Flaggr Sinn und Zweck f ixed Gleitkomma-Format scientific Exponential-Format showpoint nachfolgende Nullen bei Gleitkommazahlen ausgeben AvkUJbclprmatc far ClrilpurdcUahlen Und zu guter letzt noch ein paar Flaggen aus los: ibasefield für die ausgerichtete Ausgabe. Vorausgesetzt natürlich, es wurde eine größere Weite (widthO) verwendet: left Ausgabe erfolgt linksbündig, right Ausgabe erfolgt rechtsbündig. internal Wird ein Vorzeichen verwendet (showpog). wird der Raum zwischen dem Vorzeichen und dem Wert mit Leerzeichen aufgefulh. Axrtgabrlorniäte lum AuUichttrt U»9<ng alt Strnn und Dateben 881
Jetzt ist es raus An dieser Stelle hast du jetzt einiges über die Ausgabe erfahren. Du kennst dich jetzt zumindest ein bisschen mit der Klassenhierarchie aus und weißt, dass die ganze (Ein* und) Ausgabe (Iber Ströme (engl. Streams) realisiert wird. Das Thema habe ich hier zwar jetzt nicht breilgetreten, aber für die Verwendung der Ströme ist das zunächst auch nicht dringend erforderlich. Okay, das Thema war jetzt nicht schwer, aber trotzdem wollen wir ein paar Aufgaben mit einbauen. {Einfache Aufgibe) Betrachte den folgenden Code und die Ausgabe dazu. Pass den Code dann an! double dvall - 123.12, dval2 - 234.23; cout.widthf10); cout.fillC .'); cout « "|” « dvall « ”|" « endl; cout.widthf10); cout « ',| + " « dval2 « *|” « endl; cout.widthf10); cout « « dvall+dval2 « ”|" « endl; Die Ausgabe dazu lautet: Krs hnung Schroedinger S ,/calc .......1123.121 ........1*234.231 ........I-3S7.3SI Schroedinger $ •I Das Programm be* der Ausgabe 502 FoxrttHh
Die Ausgabe ist allerdings nicht so, wie wir uns das vorgestellt haben. Was kannst du machen, damit die Ausgabe so aussieht? A '*> Rechn«x>e Schrocdinger $ ,/colc 1.........123.121 1+........234.231 1........... Schroedlnger S Die ÄuijiJbe tAubrf formatiert cout.setf(ios::lcft); |Z<tUf] Ein Tipp, du musst nur eine Zeile htnzufügen. Perfekt! Endlich hast du aufgehöri, kompliziert zu denken, und nimmst das. was €+♦ dir anbietei. Was passt hier nicht? int ival = 100; ios:;fmtflags nFcnt ios;;showbasc | ios;;hex | ios::oct; cout.flags( nFesc ); cout « ival « endl; t»r U»9«nj »it Str««», unq M3
Völlig richtig^ Du hast deine Hausaufgaben ordentlich gemacht. (EiRfichr AuJgjbtJ Du hast auch erfahren, dass du eine Menge an Flaggen über los: i fmtf lag« schwenken kannst. Hierfür stehen dir einige interessante Elementfunktronen zur Verfü- gung. Nenn mir die Funktionen und deren grundlegenden Zweck' Okay, dann wollen wir mal ... Mit f lags ( ) kann ich einzelne Flaggen oder eine Bit-Maske aus Flaggen setzen oder den aktuellen Zustand zurückgeben lassen. » Mit setf () kann ich die Flaggen aktivieren, " Und mit unset f ( ) kann ich Flaggen wieder deaktivieren. (fiaUchr Aulf+be] Ein letzte Frage noch. C++ bietet dir auch eine Möglichkeit an, einzelne Bytes oder ganze Stöcke auf den Ausgabestrom zu schieben. Welche Funktionen kennst du dazu? ** Mit put ( ) kann ich einzelne Zeichen unformatiert in den Ausgabestrom schreiben. » Mit write ( ) hingegen kann ich ganze Blöcke unformatiert ausgeben. JBeloMunfl Prima, Schrödinger! Der Abschnitt Ist dir sicherlich sehr leicht gefallen. Im nächsten Abschnitt erfährst du dann mehr zu den Eingabestromen. Bis dahin würde ich dir empfehlen, dir mal wieder ein wenig Spaß zu gönnen. Wie wäre es mit einer Runde WoW?! 504
In diesem Abschnitt soll jetzt die Fließrichtung des Stroms ge- ändert werden. Statt des Ausgabe- wird jetzt der Eingabestrom behandelt. Auch bei der Eingabe eines Stroms handelt es sich zunächst um nichts anderes als eine unleserliche Folge von Bytes. Hierbei hält die Klasse istreamdie Spezialisierungen der grundlegenden Datentypen für den Operator » parat. Du wirst nun zunächst nicht viel Neues gegenüber ostream erfahren, well wir im Prinzip nur die Stromrkhtung ändern. Hier ein Ausschnitt davon aus dem Header <istream> vom »-Operator: « • • istreaml operator»(const char*); istreami operator» (char&) > Istreami operator» (Inti); istreaml operator»(f loati); Auch gilt, dass dank der Referenz als Rückgabetyp wieder die Reihenschaltung möglich wird Auch hier gilt, dass es sich bei den dargestellten operator»- Funktionen um Mitglieder einer umgebenen Klasse (istream) handelt und nicht um eine „freie“ Funktion. int Ich, du; ein » ich » du; lAbU«*] Und auch hier habe ich dir bereits gezeigt, wie du den »-Operator für eigene Klassen überladen kannst. Natürlich gibt cs auch hier für ein Einlesen von rohen Bytes oder ganzen Blöcken von rohen Bytes nützliche Elementfunk- tionen. Zuin Einlesen von einzelnen Zeichen oder Bytes (inklusive der Zwi- schen™ umzekhen) kannst du die Ele- mentfunktion istreamfit get() verwenden. U«9«nj «1t 505
char ch; while (cin^etCch) ) { *1 if{ ch « ’# ) { *2 *1 In cir*cr while Schleife werd« n hier einzelne Zeichen vorn Eiogibe- stRFn (Hier ein) ungelesen ein.putback(1 I '); *2 } *2 Hier überprüfst du. ob das em^rlescne Zeichen ein i$t Tnftt das zu. schieben wir nicht dieses, sondern das Zeichen ! zuruck m eise { ’S cout.put(ch); } cmen Erngabestrom, so dass es beim nächsten Einlesen w»edcr zur Verfügung steht. h ermlt ersetzen wir quasi das Zeichen # durch das Zeichen t im Puffer 3 Das allgemein zuletzt eingelesene Zeichen kannst du auch >u . , mit istreamfic unget () wieder in den Puffer zurücksch eben. •3 Alle anderen Zeichen ° außer werden über das Gegenstück von IHanWrgrundintal In der Praxis wird nicht einfach eine while-Schleife zum Überprüfen auf eine korrekte Eingabe verwendet. Hierfür gibt cs in ios einige Status* flaggen, um zu testen, ob und welcher Fehler aufgetreten ist. Darauf gehen wir aber noch extra ein. Neben putback() und unget () gibt es noch die Elementfunktion pcek(), mit der du das nächste Zeichen betrachten kannst, ohne es aus dem Eingabestrom zu ent- nehmen. Es bleibt also im Puffer, bis du es mit dem nächsten get( > herausntmmst Von get ( ) gibt es auch noch eine überladene Version, mit der du eine bestimmte Anzahl an Zeichen einlesen kannst. Auch ein eigenes getline() (Ihnlkh dem string: :gctline ()) für den Eingabe- strom steht dir zur Verfügung. const unsigned int NC 16; char puffcr(NC]; • • • ein.get(puffer, NC); *1 ’1 Damit fcinml du NC Zeichen au*. d<-m finß.ibcitrorn iriden Puffer puffer speichern. Et werden entweder NC Zeichen cingrfetttfi «ler biv jy einem IrM- gekglen Zeichen, was stanff^rdnuSig das Zeichen 1 \n1 Isl oder «1$ Qpbofljtre drtte| Argument von get ( ) angegeben werden kann Öle Syntax lautet istrcam& gcttchar^^unsigncd int* char c-^n1) cin.getline(puffer» NC); *2 |H«nter^rundln^o| 8e«de Elementfunktionen, get() und getlinef). schreiben stets das String-Endezeichen '\0' mit in die Puffervanable. ’2 Zwischen der Elementfunktion get 1 ine ( ) ;bi«r n rh’ mit dc< string t: getline () Version glclchselien) unö der Flernentfunttion get (> gibt es keinen Unterschied Auch die Syntax entspricht der von get () In der Praxis wird allerdings gctlinc () häutiger verwendet. weil eben erkenntlicher ist. was diese Funktion macht. 506 Ut;t«l FOXFZtKH
Wo Ist mein Input? a/s C-’S Du hast jetzt die Grundlagen zu den Eingabeströmen Iccnnenge- lernt. Ich habe übrigens bewusst darauf verzichtet, bei der Ein- gabe von der Tastatur oder vorher bei der Ausgabe vom Bild- schirm zu sprechen, weil das Stromkonzcpt nicht allein darauf festgelegt ist. Aber wie flexibel diese Ströme sind, wirst du dann im Laufe der nächsten Abschnitte selbst feststellen. Nun natürlich noch ein paar Aufgaben zu den Eingabeströmen. (Schwierige Aufg-ibrl Lies jetzt so lange zeichenweise Text in einer Schleife mit get() ein. bis ein 1 . ’ eingegeben wurde Ausgeben sollst du dann nur den ersten Buchstaben eines jeden Wortes. Zahl die einzel- nen Buchstaben mit, ignorier dabei aber die Leerzeichen. Gib am Ende einer Zeile dann auch die Anzahl der Buchstaben aus. Und: Es ist dir jetzt nicht gestattet, auf string zurückzugrei- fen! Du sollst jetzt mit den rohen Zeichen spielen üben. Jetzt sei kein Frosch. Schrödinger! Fang einfach damit an, in einer while Schleife über get ( ) die Zeichen ein- zulesen. und dann kannst du die einzelnen Buchstaben sezieren Pass aber auf, dass du dich nicht in einer (Endlos-) Schleife verhedderst! Per U«gjng Str9«a> und D*t*b*rs 507
1 Da Jcomrrwn die einzelnen /eichen rein. en < *6 char ch int ent while(cin.get(ch)) { *3 if( ch » ) { *4 break; ‘4 } ’4 eise if( ent 0 ) ( cout « ch; *5 cnt++; *5 > *5 eise if( ch — *\n’ ) cout « « ent; cout « ch: ‘6 ent = 0 > *6 eise if( ch cin.get(ch); "8 if( ch — ’\n* ) { *9 cout « •’s" « ent « ch; *9 ent " 0; "9 } *9 eise if( ch == ' " || ch cin.ungetO; *10 } *10 eise if(ch — { '11 break; ‘11 } '11 eise { *12 cout « ch; *12 cnt++; '12 l > *12 } eise { *1 cnt++; } '13 > ( '10 *13 Ist es kein Punkt, kein Newfime-Zeichen, kein Leer, oder Tabulatorzeichen, dann ist es nur eil weshalb wir den Zahler nur um 1 erhöhen 13 “2 ... und hier zahle ich die eiru<-Inen ÖuchtUbcn. 3 Hier starte ich mit dem zeichenweisen Einlesen vom Emgabestrom (h er ein) *4 Hier lieh« du die Abbruchbedingung untere« Schleife, ,venn ein *. ’ ah Zeichen eingegeben wurde. ’5 Das Zeichen m der Zeihe soll immer ausgegeben werden! 6 Sind wir am Zcifencndc angclgngt. geben wir die Ani.thl der tatsächlich angegebenen Zeichen (ohne Leerzeichen) aus. löten den Zdlcnumbruch aus und setzen den Zähl« für die Zeichen zurück auf 0 ? Wenn wir «n LceWrhen orfer e>n Tabtilatorrckhen gefunden haben, wollen den okhtien Anfangsbuchstaben eines Wortes haben 6 . .. und leien den mal schnell ein. '9 Das kann natürlich auch ein Zeilenende se.n. worauf wir, wir gehabt, reagieren wurden ib Oder ist da en weiteres Leer- oder Tahu’jtor- zclchen. dann schieben wir es wieder luröck in den Eingabestrom, um dieses beim nächsten Durc.h'au1 zu behandeln. 11 Finden wir hinter den Leer- oder Tabul.itorzeichen auch wieder einen Punkt, sind wir hier auch fertig und brechen die Schiede ab. 12 Ansonsten kann »ich hinter einem Li trift Tabulatorwicben nur ein gewöhnlicher Buch- stabc befinden, den w=r dann auch ausge neo. woraufhin wir den Zahler «höben. 508 taoita l Fort ZC HW
Das Programm bei der Ausführung: Prima Schrödinger! Fast perfekt! Als Verbesserung könnte ich dir hier nur noch empfehlen, zu überprüfen, ob nicht auch der erste Buchstabe ein Leerzeichen ist. Ansonsten top! An der Stelle will ich dir noch eine neue Funktion vorstellen: istreami ignoretint n • lt int t • EOF); Damit werden n Zeichen aus dein Fingabestrom bzw. bis zu dem Zeichen, welches in t enthalten ist. überlesen. Das Zeichen in t wird dabei aus dem Puffer entfernt. IMotllJ EOF Ist ein Makro, welches mit dem Wert -1 vordefiniert wird, und dient dazu, das Dateiende (End Of File) anzuzeigen, Dir U»9<ng Strtm und httkn 609
In der Praxis wird ignoref) z. B. oft angewendet, um ein .Netie-Zeile-Zeichen* aus dem Puffer zu ignorieren. Guck dir bspw. den folgenden Codeausschnitt dazu an: const int LINE - 255; char text![LINE], text2(LINE); cout « "Gib etwas ein: cln.get(ttxt1, LINE); cin.ignore(LINE, ’\n’>; *1 —— cout « "Gib noch was ein: ein.get(tcxt2, LINE); *2 cout « "textl : " « textl « endl; cout « “tcxt2 : " « text2 « endl; *1 Hlefmrt ignorimt du mindrvtcns die narhstr LINE-Arv.ihl an Zechen Chcse werden dann verworfen, wenn em Ncue-ZcHc-Zcichcn gefunden wurde. Auch das Neue-Ze»ie-Zeichen wird hiermit dann verworfen. Wenn du diese Zeile hier rausnimmst, kann es passieren, dass das Newline-Zekhen Puffer bleibt und sich somit .... *2 . . dieses jwe-rte get ( ) einfach dieses Wcwlirw’ Khnappt und quasi übersprungen wird. IBeloheurtgl Djs Listing zuvor hast du echt toll gelöst* Zur Belohnung gibt es heute wieder Schnitzel mit Pommes zum Essen. 510 c«o;f I FQMFZChW
Jetzt ist es drin const int LINE 32; char text![LINE); cout « "Gib noch was ein: ein » tcxtl; Ich denke mir, dass dir dieser Abschnitt mit dem Eingabcstrom der Klasse istream nicht allzu schwer gefallen ist, oder? Das Konzept ist wirklich angenehm überschaubar Implementiert. Zum Schluss will ich dich daher noch mit ein paar einfachen Strom- fragen belästigen. ItiRfiche AulpM Beim folgenden Codeausschnitt gibt es immer Probleme beim Einlesen, weil alles nur bis zum ersten Einlesen eines Leerzeichens gespeichert wird. Welche Alternativen hast du? Und wie kannst du dieses Beispiel „sicherer" machen? ^Of istream::get() *^v,SSz tc^i 4^$%^ string Prima, Schrödingerl Eine wichtige Notiz muss ich noch hinzufügen: IZttlcF] Für das Einlesen einer ganzen Zeile von string musst du natürlich die Element- funktion string: :getline() statt istream: :getline() verwenden. D«r U»9«nj «11 Str,««, und ht«l,n 511
Itiafachr Aulpbe] Der folgende Codeausschnitt zeigt dir jeweils ein Beispiel mit 1$Cream:;getline(> und eines mit string::getline() Was wird bei diesen beiden wie lange und wie viel davon wird eingelesen? const int LINE 64; char textl[LINE); string textZ; 1 Was passiert hier?! ein.getLine(textl, LINE, cout « textl « endl; getlinefcin, text2, * cout « text2 « endl istream ’e’. Sehr 'guV, Schrödinger ! Du bist echt gut drauf mittlerweile! I BHobnun^l So, jetzt bist du auch mit deinen Eingabeströmen durch, ohne dass du einen Stromschlag bekommen hast. Zur Belohnung solltest du dir mal wieder etwas kaufen. Wie wäre es mit dem neuen Rechner, von dem du schon so lange schwärmst? Oder das neue Smartphone, das du schon so lange haben wolltest? f*i textl 512 roxrzcHW
Manipulatoren sind direkte Zugriffe, welche du in die Eingabe- oder Ausgabe- ströme einschieben kannst. Du erinnerst dich doch sicher noch an das Setzen von Flaggen? Folgendermaßen erfolgt bspw. die Ausgabe eines integer-wertes in hexadezimaler Schreibweise: int ival 255; cout.flags(los::hex); "1 cout « ival « endl; ’2 "1 Hier wird die Flagge fwr den Strom cout auf ios : : hex gebeut,. .. *2 ... weshalb hier auch ein hexadezimaler Wert ausgegeben wird. Dasselbe kannst du in ähnlicher Weise auch mit dem folgenden Manipulator machen: int ival 255; cout « hex « ival « endl; *1 1 Mit dem Manipulator hex erreichst du dasselbe wir mit dem Selzefl einer Flaggr. Unri. .1. endl -St auch ein Manipulator’ Ja, du findest alle passenden Gegenstücke und ein paar mehr wieder. U»gjng «K Str»««» und
Die folgende Tabelle listet dir die Manipulatoren von <ios> auf: ManipulatorI Sinn und Zweck boolalpha noboolalpha Gibt "true^false* im wörtlichen Sinne oder »Is numerischen Wert (1/0) aus/»m. showbase noshowbase Blendet die Zahlenbasrt ein oder aus. Bei hexa- dezimaler Basis (hex) wirt dies Ox und bei oktaler (OCt) Darstellung 0. showpoint noshowpolnc Wenn eine Nachkörnmazahl mit Nullen endet, werden diese hiermit ein-/ausgeblendcl, showpos noshowpos Bei positiven Zahlen kann hiermit das * davor ein-/ausgeblendet werden. skipws noskipws Ignoriert oder beachtet das ZwiKhenraumzeichen- uppercase nouppercaso GroA- oder Kleinschreibung von e. x, a. b. c, d( e. f unitbuf nounitbuf Leert nach jeder Ausgab* den Puffer oder puffert eben. dec dezimal oct oktal hex hexadezimal internal Füllt den Raum zwischen Vorzeichen und Werl auf. Vorausgesetzt natürlich, es wird showpos verwendet und eine größere Feldbreite (mitwidtho oder SCtwO). left Richtet die Ausgabe linksbündig aus. right Richtet die Ausgabe rechtsbündig aus. f ixed Ausgabe im Gleitkomma-Format scientifc Ausgabe im fxponential-Format Dien .Mj - i nd i -i Header <i.OS> enthalten 814 t>o;t«l
In <iostream> findest du folgende Manipulatoren: | Manipulator 1 Was er kann ... 1 endl Gibt eine neue Zeile aus. ende Gibt das Zeichen 1 \ 0’ aus. f lush Leert den Ausgabeputter. WS Ignoriert das Zwtjchenraumzeichen beim Einlesen. Oleir V-jA.pui.)!o->en vnd .n. H«idtr <lOSt TCiJin* nth.Uen Und natürlich findest du auch Manipulatoren, um das Füllzeichen oder die Weite festzulegen. Hierzu musst du allerdings auch den Header <iomanip> mit einbinden: 1 Manipulator 1 Was er taugt... | setiosflags<los::£mtflags m) Setzt die tn Hl angegebenen Flaggen, Mehrere Flaggen können, wie gehabt, mit dem bitweisen | verknüpft werden. resetiosflags(ios::fmtflags m) Loscht alle in m angegebenen Flaggen. setbascfint base) Damit kannst du die Zahlenbasis setzen, bspw. 8 (oktal), 10 (dezimal) oder 16 (hexadezimal). setfill(char ch) Damit kannst du das Fullzeichen setzen. Ist eine Alternative zu f i 11 ( ). sctprecision(int n) Damit legst du die Anzahl dec Nachkqmma- stellen fest. Ist eine Alternative zu precision(). setw(int w) Damit legst du die Weite zwischen zwei Werten im Strom fest. Ist eine Alternative zu width( ). Weiter* W.nipt. itoren ev> dem He.der < ioniiinip> t««’ U»9«nj «it Str«««, und E.teier» 618
Manipulation Ist alles... Die Verwendung der Standardmanipulatoren dürfte dir eigentlich kein großes Kopfzerbrechen mehr bereiten. Und wenn du dir dabei einmal nicht ganz sicher sein solltest, dann probier es doch einfach selber aus. wie etwas funktioniert. Hier ein kleines Beispiel dazu: linclude <iostreacn> linclude <ioraanip> *1 1 De/ Hcade* wi*d für den MimpulÄlör SCtw( > benötig«. int val - 100; string elnTexc; cout « showpos « internal « setw(10) « val « endl; ’2 cout « "TexteAngabe: ; ein » ws » einTexc; f3 cout « einText « endl; "2 MjI showpos v»rdein Pluszeichen vor den Wert ges-etzi. Durch internal wird der *3 Mit WS sorgst du dafü*. dass die führenden LeefieKhen beim Singabestreu ignoriert werden Sprich, gibst du ein Lee*- oder Tabulator Zeichen für d<?n Text ein. wird dieses nicht mit in Raum zwischen diesem Vor- zeichen und dem Wert auigefüllt. Und rw*r aufgctcilt auf msgewimt zehn Zeichen, wie dies mit dem Manipulator setw( 10) vorgegeben wird. Dann wud endlich de* Wert va 1 in den Strom geschickt und ganz hinten schieben wir noch endl für einen Zeilerwwschub em. Jawohl, das kannst du! Das Erstellen von Manipulatoren ohne Parameter ist sogar ein Kinderspiel, Du brauchst hier- bei nur eine Referenz auf den Strom zurückzugeben und natürlich auch eine Referenz vom Strom auf den Manipulator zu übergeben. 516 FOMFZCNW
Hierzu zwei sehr einfach gehaltene Beispiele mit ostream und istream ohne Parameter: oscreaml myhcxtoscreamS os) { *1 os « showbase « upperease « return os; *3 *1 Ein ostream komm! rein, und ein ostream kommt Mieder raus hex; *2 "2 H»er erstelien wir eine spezielle Version einer hexAdwiiru.lcn Zahlcmusgj.be, bei der der hexAdwurule Zahlenwert gleich nmsamt der ßaM5 und in Großschreibung angezeigt wird (bspw. OXFF lür 255). Den so manipulierten Strom geben wir rutu*l»ch wieder an seinen Aufrufer zurück > istreami cat_f1rst_letter(istrearot is) { ’5 is.getO; *6 ’4 Und hier ist unser selb« geschriebener Manipulator für den AusgabesVom «m Einsatz ’$ Andersherum ist d*es natürlich ähnlich Refn geht c n istream und rautsdi auch rin istream return is; *7 > *ß Wir machen nichts anderes, ab den ersten Buchstahrn zu ^verschlucken-.... I ‘7 ... und geben dann auch hfer den Eingabe-Strom wieder an den Aulnder zuruck. cout « myhex « val « endl; *4 string str; ein » eat_first letter » str; *8 cout « str « endl; “8 Und hier finde« du unseren selbst geschriebenen Manipulator für den Eingabrtfrom fm Einsatz. Dieser macht nichts anderes, als den eisten Buchstaben zu .verschlucken“ und nicht im String str zu ipwthem. Dass dies funktioniert, liegt an folgender internen (komplexe- ren) Implementierung des Ausgabeoperators |N<rtiz| l AMthilfe des Eunktionszcigers r-fp wird diese Punktion Im I Strom dann aufgerufen. Ähnlich ist das natürlich auch beim Eingabcopcraror implementiert. Ja, das geht auch, doch cs ist etwas komplexerr lässt sich aber übereinen Funktor problemlos realisieren. ostreami ostrean: :ostream«(ostreaa£ (*fp) (ostreansfc) ) ( *fp(*this); *1 return *thiss > Ohne das Thema jetzt zu vertiefen; Ein Funktor ist ein altbekanntes Mittel, ein Funktionsobjekt. Funktionsobjekte sind im Grunde wiederum nichts anderes als eine Erweiterung der Funktionszeiger (Zeiger auf Funktionen) aus der alten C-Zeit Allerdings verwendet man als C+♦•Programmierer natürlich keine alten Sachen mehr. In C++ benutzen wir statt des Funktionszeigers eben den Funktor, und dieser ist ein Objekt, welches operatorf) definiert. In der gängigen Praxis werden solche Funktoren häufig in Verbindung mit Algorithmen der Standardbibliothek verwendet. t.r U»,,ng »>t Str»,», und 6«t.t.r 517
Hierzu nun ein Beispiel, wie du einen Manipulator mit Parame- tern in der Praxis schreiben kannst: dass nendl ( *1 ’1 Wie du hier sehen kannst. private: handelt es sich um eine ganz int Lines; gewöhnliche Klassendefinition V public: nendl(int n=l) : llnes(n) {} ostreami operator!)lostreamir os) const { *2 forf int i-0; Klines; i++) { os « ’ \n*; } return os; } }; ostreamk operator«! ost reamk return line(os); ’3 > os» const ncndli Line) < 'S Das ul der Funktoraufruf "2 ... welche nur durch die Anwesenheit von operatorf ) ein Objekt vom Typ nendl automatisch ju einem Funktor macht. '4 Der selbst geschriebene Manipulator im Einsau. n unserem Fall macht dieser nichts anderes, als die vorgegebene Anrahl an Zcllcnumbruchen ausiulOwn dass eat_more_letter { *5 private: int letters; public: eat_more_letter(int n=l) istreatn& operator!)!istreami is) const { *5 forf int i“0; l<letters; { is.get(); } return is; ) : letters(n) {} >; istreatni operator» (lstream& is» const eat_more_letter$ letters) { return letters(is); *5 > cout « "Ein Text" « nendl(3); *4 cout « "nach ein paar endlf) tiefer" « endl; *5 Dawlbe wird hier «uch bn d*c Andw facHung UingabciVoni) denwitfftert In <f»ewm Beispiel kannst du jeut eit*e bestimmte Anzahl an Zeichen vorn Anfang del Stringi .vmchlucketT. tfimit difsr nbch! geipeicbert werden string str; ein » eat_more_letter!l) » str; *5 cout « str « endl; S18 Capital fWftHh
Das Programm bei der Ausführung: Okay, ich erkläre cs dir anhand von endl (3) etwas genauer. Wenn du endl(3) aufnilst, wird ja logischer- weise zunächst mal ein Objekt endl erzeugt und mit dem Wert 3 initialisiert. Intern wird dann der Ausdruck cout « endl(3); umgewandelt in operator«(cout, endl(3)) und somit an den überladenen Operator weitergereicht. in der Operatorüberladung wird dann der Aufruf von line(os) umgewandelt zu 1inc.operator(os) (w-oeiji Du solltest auch hier eine Referenz als Rückgabewert verwenden, weil du nur so sicherstellen kannst, dass du weitere Operatoren verketten kannst! Umging «fct StruM und Ditekn 519
Ordentlich manipuliert Du kennst nun die verschiedenen Manipulatoren, die du deinen Strömen zur Abarbei- tung spezieller Funktionen einschieben kannst. Falls du einen Manipulator vermissen solltest, weißt du Jetzt außerdem, wie du selber einen mit oder ohne Parameter erstel- len kannst. Natürlich gilt auch hier wieder, dass diese Manipulatoren für alle Ströme anzuwenden sind, nicht nur, auch wenn es als klassisches Beispiel hier zunächst immer gezeigt wurde, für die klassische Ein- und Ausgabe via Tastaturund Bildschirm. Okay, Zeit für noch ein paar Übungen: (Einfache Aufgabe] Im folgenden Listing findest du zwei Beispiele von Manipulatoren. * 3 Kannst du mir sagen, was diese ausgeben werden? linclude «Lonanlp* ’i w» wM •»*<?* wohl ausgegeber»? inc val 255; double doubly « 22.22; cout « setw(lO) « setfill('F’) « sctbnse(16) « uppercase << left « val « endl; ’1 cout « noshowpos « showpoinc « fixed « secprecisionf10) « setfill(’*‘) « setw(20) « right « doubly « endl; *1 ------. .FFhhFFFkF^ Fi FÜtft^cZ,. 520 FoKrztMW
£.0 ' SC»*«// *******^ ^OOOOOOOO *. Perfekt, SchrödingerJ Sehr gut aufgepasst. iSchwitfig* Atf gibr| Guck dir folgendes Beispiel an. Das Listing lasst sich zwar aus- führen, aber hier funktioniert der selbst geschriebene Manipula- tor myscicntiflc nicht. ostream myscicntifiefostream os) ( os « scientific « showpoint « « setprecision(S); return os; > uppercase endl; double doubly - 123,12; cout « myscientific « doubly « Okay. Ich helfe dir, Erinnere dich, was kh dir Ober die Referenzen in Bezug auf Manipulatoren gesagt habe. *1 Djßk jeweih einer Rcfrrrrv rein und einer WU klappt C5 jefrt auch mit dem Afaniputetocl ostreami myscientificfostreami os) < .} *1 iBeloKnurtgl So, zur Belohnung kriegst du jetzt eine Nackenmassage Dir U»gjng Strtm un-d 521
Auch bei den Strömen der Ein und Ausgabe können Fehler auftreten. Glücklicher- weise stehen für diesen Fall auch einige Mechanismen zur Verfügung, um den Zustand aller Arten von Strömen abzufragen. Hierzu findest du in los mit dem Typ los täte einige Flaggen vor. 522 roaflCKt,
Folgende Flaggen sind dabei in los durch den Aufzählungstyp iostate iniert: 1 F|a|®r 1 W« sie bemerkt... ios::goodbit Alles bestens! Keine Probleme! ios::eofbit Das Ende des Stroms ist erreicht. ios::failbit Der letzte Ein- oder Ausgabestrom war fehlerhaft. ios::badbit Der Strom ist kaputt! Schwere» Fehler! 1K ilv ioS <ur riif Auf die einzelnen Flaggen zugreifen, um deren Status zu über- prüfen, kannst du mit folgenden Elementfunktionen: Verschiedene ElementiunMronen. lern den Zurtind eines Stroms ibzufragen (Zettel] Bei Fehlern aufseiten des Ein-ZAusgabesystems der iostroacn- Bibliothek kann es auch sein, dass die Ausnahme los_ base : : failure geworfen wird (wenn also ios : : badbit gesetzt wird) Allerdings hängt diese Implementierung vom Compilerhersteller ab. Trotzdem kannst du diese Ausnahme auch selber in deinem Programm werfen und auffangen. Der Umgang Strom und htikn 523
Erst den Strom abstellen... ... bevor wir daran arbeiten. Natürlich gilt auch hier, dass die Überprüfung, ob der Strom ordentlich fließt, auch auf alle Arten von Strömen angewendet werden kann. Hierzu ein einfaches Beispiel, das verschiedene Arten von Strömen in einer Funktion überprüft: linclude <fstream> •1 Eine e ntici'v .ion. wrklWalle Arten Behend von F i°s ' Alle davon rqmktaHen rL' i Strömen, ai der Baynkl.1 uerpriilen koni ihgeleitctm $ können h«cr praktisch verwend« werden void löstateCheck(ios Sstrom) { *1 if( Strom.good() ) ( *2 cout « "Strom ist in Ordnungen"; > eise if( stroa.badf) ) { *3 cout « "Fataler Fehlerl Stroinausfalll \n"l > eise if( stron.fail() ) ( *4 cout « "Fehler beim Strömen”; | > eise if( stroo.eofO cout « "Strömende > ström.clear(); ’6 > > < *5 erreichten”; '6 Setrt die Fehlermaske int cinWcrt; cout « "Wert eingeben : 11; ein » einWert; lostateCheck( ein ); *7 ifstream file; f ile* open("Gibcesnlcht -txt"); iostateCheck( file ); ’8 *2 Ke«n Problem mit dem Strom , los: :goodbit)' *| Ein fataler fciler in aulgetreten ‘ ios: : badbit»! *4 Ein Fehler wurde bei der l< dem (los: los a8 ifstream ist ein iVie-hr diiüber erfährst d natürlich davon ausge QifrJnfiiCM. ber nicht eomti die Elementfunktion tefi Operation auf Itrom ausgeSOst failbit oder :tbadbitr 5 j.K Strome {ios: r hen ein, kliv. Lesen. wi<d die Datei somit wieder ) aktiv wird. "7 Hiermit überprüf« du deinen Emg^besirom ein Gib z. fl. keinen Integer, sondern ein ch wird die Elemcntfunktion f a Bei korrekter Eingabe wm goodf ) höre 524 FomrrcMk
Die Sicherung Ist geflogen tZrtui) Du kannst natürlich auch den Zustand des Streams (besonders nach dem Öffnen) durch die Umwandlung nach bool mit dem ! -Operator testen Das wäre dann eine Überprüfung auf fallt). Zugegeben, das Thema „Überprüfen der Flaggen von iostate‘ ist nicht sehr komplex und überschaubar. Aber trotzdem wollte Ich dir das Ganze in einem extra Kapitel, losgelöst von den anderen Stromkapiteln, demonstrieren. An dieser Stelle will irh noch ein paar Worte über das Bit LOS : xeofbit verlieren, welches ja gesetzt wird, wenn das Ende eines Stroms (EOF) erreicht wurde. Bei einer Datei dürfte es ja irgendwo logisch sein, dass hiermit das Ende einer Datei gemeint ist. Aber auch beim Einlesen von der Tastatur kannst du setzen, weil diese Tastenkombinationen einen Wert ungleich 0 zurückliefern. Guck dir bspw. folgenden Code an: char ch; while( cin.get(ch) ) cout.put(ch); *1 > *1 if( cin.eofO ) ( '2 cout « “Ende des { *1 •1 E«nc Schleife, die mit get ( ) Zeichen für Zeichen vom Strom ein einliest und mit put () wieder auf dem Strom COut ausgibt Abbrechen kannst du den Vorgang, indem du hier Streams erreiche • EOF\n"; drückst, ... *2 . und »mit EOF (.End of A/r) auslöM und d.irrnt den Strom berrvtot. Die Elrnbcntfunktion eof ( ) bwenL daw hic*bo da* Bit los: : COf bit gesetzt wurde IZcUtl] Die Tastenkombination strg + o wird gewöhnlich über eine interaktive Shell (bspw. Umx-Shell) als EOF interpretiert. In einer MS-DOS-Konsole hingegen kann dieses EOF über sus |+ z erzeugt werden. Mr Haging Strikt und httkß 625
Wo man hinsicht, nur Ströme. So verwunden cs nicht, dass diese auch in Dateien hinein- und wieder herausfließen können. Auch das wurde natürlich auf dem iOS- Konzept aufgebaut. Bisher kennst du nur die Möglichkeiten, die Daten in verschiedenen einfachen und komplexeren Typen im Arbeitsspeicher zu halten. Sobald das Programm allerdings beendet ist, sind auch diese Daten futsch. Für eine dauerhafte Sicherung der Daten musst du diese in einer Datei auf einem externen Daten- träger sichern: Dauerhafte Sicherung von Daten auf einem externen Speicher Vielleicht interessiert es dich auch, wie die Daten in die Datei kommen. Hierfür benötigst du natürlich entsprechende Dateifunktionen (Operationen), welche dir von der C++-Bibliothek zur Verfügung gestellt werden. Damit kannst du sowohl lesen als auch schreiben aus bzw. in Dateien. Natürlich wird gewöhnlich nicht Byte für Byte auf 426 FOMrttKh
einen externen Speicher geschrieben oder gelesen. Solche Medien arbeiten block- orientiert (Daten werden in ganzen Blöcken übertragen). Daher wird für die Über- tragung von Daten ein (Datei-)Puffer im Hauptspeicher für die Zwischenspeicherung verwendet. Die Datei selbst ist wieder zunächst nichts anderes als ein Vektor bzw. Amy von einzelnen Bytes mit einer bestimmten Lange. Um die Formatierung und Strukturierung der Datei musst du dich selber kümmern. Das heißt, wenn du Daten in einer bestimm- ten Struktur in der Datei speicherst, musst du diese logischerweise auch wieder In dieser Form auslescn. Damit cs mit dem Lesen und Schreiben auch klappt, wird eine Dateiposition (angefangen mit dem Byte 0) benötigt. Nur so ist gewährleistet, dass eine Datei sequenziell abgearbeitet werden kann. Wenn du eine l.ese-Operntion durch- führst. wird auch die Lese-Position auf das nächste Byte gesetzt, welches noch nicht gelesen wurde. Beim Schreiben ist das genauso. Ganz am Anfang des Kapitels, in der vereinfachten Darstellung der Klassenhierarchie, konntest du sehr schön erkennen, dass die drei Ströme if stream. ofstream und f Stream (das f steht immer für Hie; also Fiiestreams) alle bereits bekannten Stromklasscn als Basisklasse haben und somit auch alle bereits bekannten Funktiona- litäten (wie bspw. die Operatoren << und » zum Schreiben und Lesen) aus den Abschnitten zuvor verwendet werden können: " if stream ist von istream abgeleitet und eignet sich für das Lesen von Dateien. of stream ist von ostream abgeleitet und für das Schreiben Dateien gedacht. “ f stream ist von iostream abgeleitet und somit für das Lesen und Schreiben von Dateien geeignet. t*bUg*> Für alle Dateiströme musst du den Header <fscream> in deinem Programm mit angeben. Um eine Datei zu verwenden, musst du diese (logischerweise) öffnen. Beim Öffnen gibst du den Dateinamen (optional mit Pfad) und einen optionalen Modus an, wie du die Datei öffnen willst. Uaginf alt Straaas und tatalan 527
Verwendest du keine Pfadangabe, wird davon ausgegangen, dass sich die Datei im selben Verzeichnis wie das Programm befindet. Das öffnen einer Datei kannst du entweder mit der Fdemenifunktion open( ) oder gleich mit dem Konstruktor realisieren. Im Folgenden zeige ich dir ein paar Beispiele, die sich alle sowohl mit ifstreatn als auch mit ofstream oder fstream anwenden lassen: ’1 Hier wird <*m nrurf Dateiltröm nim Schreiben .ingrlrgt Existiert diese Datei nicht, wird sie angelegt. Aber Achtung’ Existiert die Dale» festere* dat. wird sie auf die Länge 0 gesetzt und überschrieben’’’ Der Dateiname wird immer als C-Siring gegeben. .2 Dirnjt du tinen Dateistrom linclude <fstream> ofstream eineDateiOl("testdatei; *1 ifstream eineDateiOZ("'nocheinedatei.txt"1); "2 if ( 1 eineDatciO2 ) { "3 cerr « 'Tatet existiert nicht!!!\nM; > ium Lesen Jin rxxheintyfjlev txt an. Dir Datei befinde’ sich hier im sdben Verzeichn^ wie dM Programm. Ist die Date« geöffnet, befindet sich die Dateiposition am Anfang (Byte 0). ’3 Existiert die Datei noc^nedatei.trt nicht, gibt diese if-Überprüfung true zutück. und eswwd eine entsprechende Meldung ausgegeben. fstream eineDacei03(rtjunkFile«.txtrt); *4 if( eineDatei02.fail() > { *5 cerr « "Gibt cs hier auch nicht!!!\n"; > ifstream elneDatei04; ’6 clneDateiO4,open("Cibteshierauchnicht.dat"); if( eineDateiO*.fallt) ) { *8 cerr « "Nope» auch net vorhanden!!!\n"; } *4 H irr wollen wir d*c Datei junfc/Nc btt zum Lesen und Schreiben jffnen. “5 .. wenn aber diese Datei nicht existiert, gibt diese if Überprüfung auf f ail( ) ebenfalls true zurück, urxl es wird eine entsprechende Meldung auigegeben. *7 'S Em leeres Objekt if Strcatn wird hier angelegt “T Und hier wird mit der [lementlunktifrn open ( ) versucht, eine Datei zu öffnen. ’8 Und auch hier wollen wir überprüfen, ob dieser DätciMrpm geöffnet werden konnte oder mehr Zur fehlerüberprüfung beim Öffnen einer Datei kannst du somit sowohl if (Jfile) als auch if (file.failO) verwenden. Beide Male wird true zurückgegeben, wenn das Öffnen einer Datei fehlgeschlagen ist und somit die Flagge io»:: failbit gesetzt wird. Bis jetzt würdest du dich aufdie Standardwerte der Klassen fstream. Ifstream und ofstream ver- lassen. Wie bereits erwähnt, kannst du da auch selber cingreifen. Hierbei gibt cs Flaggen, welche du mit dem bitweisen ODER-Operator (|) auch miteinander verknüpfen kannst. 528 roxfZtMW
Hierzu sind in der Headerdatei <ios> Flaggen vom Typ ios: :openmodc definiert. ... und was diese bewirkt ioS : : in Öffnet eine Datei zum Lesen. los: :out Öffnet eine Datei zum Schreiben. ios: :app Hangt beim Scheiben die neuen Daten ans Ende. ios: :trunc Die lange der Datei wird auf 0 gesetzt und somit gelöscht. ios: :ate 5prmgt nach dem Offnen der Datei an* Ende. Ohne diese flagge befindet sich dir Position beim Lesen und Schreiben sonst immer am Anfang der Datei. ios: :binary Öffnet eine Datei im Binärmodus (RAW-Modus), Umwandlung von Zeilenende wird bspw. nicht durchgeführt. Es wird Byte für Byte übertragen. Ohne diesen Modus werden die Dateien immer Im .Textmodus’ geöffnet. Miggen zum Öffnen vor Dateien Der Standardwert von ifstream ist ios:: in und der von ofscream dem- zufolge ios::out und ios::tninc fstream hingegen hat keinen Standard- wert. *1 Hirrmil w»id eine Date* zum Schreiben (los: tont) am Dateiende lios: :upp) geöffnet. Wenn die Datei nkht existiert, wird sie neu angelegt. Und hierzu jetzt ein paar Beispiele mit den neuen Flaggen zum Öffnen einer Datei: fstroam mus^c(,’EnlSicfilo.dat,,, ios::out|ios: :app); *1 if( music.fallt) ) { /* Fehlerlü */ } of stream eineDatei; clneD«itei.open("NochclneDateUtxtrt, los::out|ios;:trunc); '2 if( l eineDatei ) { /* Fehler II! */ } fstream nocheineDatei("NichtWichtig.dat", ios::in|ios::out); *3 if( nocheineDatei. failO ) { /* Fehler!!! */ } *2 Otfncl ebenfalls eine Datei ium Schreiben (ios: :out) Ex^tlen diese, wird der Inhalt der Datei gelöscht lios: :trunc). Existiert diese n ch:. wird sie neu angelegt. *3 Damit öffnest du eine bestehende Datei zum lesen und Schreiben. Existiert diese Datei nicht, wird los:: failbit gesetzt. und die Datei wird bleibe* auch nicht erzeugt (wegen der Flagge los:: in) Umgang mit Strif!. und 629
|2«U«*J Auch wenn sich die einzelnen Flaggen kombinieren lassen, solltest du doch I Immer die Flagge ios:; in bzw. ios: :out verwenden. Zusätzlich musst du wissen, dass bei der Verwendung ios: : in die Datei bereits existieren muss!!! Verwendest du kein ios: :in, wird die Datei neu angelegt, wenn sie nicht vorhanden ist. en Datei« Wenn du mit der Datei fertig bist,, solltest du sic schließen. Zum einen stehen dir nicht unendlich viele Dateisiröme zur Verfügung, zum anderen gehen auch keine Daten ver- loren, die sich noch im Puffer befinden, die aber noch nicht abgearbeitet wurden. Hier- für kannst du schlicht und einfach die Eleinentfunktion Close ( ) verwenden, um den entsprechenden Stromstecker zu ziehen. ofstream. music("causiefile,dat"); if(muslc>iSÄOpen()) { M music4close(); *2 } z "1 Wit der Elementtunktiofl is_opcn() kannst du überprüfen, ob der Ddtei&lrom muslc überhaupt verbunden iit, ... ’2 . . und hiermit z*chcn wir den Datcl- stecker von näusic (HinCrrf rundinlpl Der Dateistrom wird auch geschlossen, wenn dass laufende Programm beendet oder der Gültigkeitsbereich verlassen wird: int main() { ostream stromOI("fllcl.txt”); { ostream. stromOZ("fileZ.txt”) ; } II hier wird scrom02 geschlossen > II hier wird stromQ2 geschlossen 530 roxfZCHW
Alles gesichert...? Das Offnen und Schließen von Dateien ist ja ein Kinderspiel. Aber auch das Lesen und Schreiben aus oder in einen solchen Strom ist nicht wesentlich komplizierter. Das Stromkonzept von C++ ist da wirklich eine tolle Sache. Sauber lesbare Sache Die einfachste Möglichkeit, jetzt etwas in eine Datei zu schreiben, ist es. wie gewohnt, die Operatoren « zum Schreiben und » zum Lesen aus einer Datei zu nutzen. Damit kannst du quasi auch sämtliche Manipulatoren und Element lünküoncn ftjrdic Formatierung verwenden. Ein einfaches Beispiel: linclude <fscreaa> linclude <cstdlib> II für exit() WWW Int val; ofstream Stecker("formatiert>txt” *1 Hier wird der D.itcötrofn Stecket* für d*c Datei fomitHrt.w zum Schreiben und Anhängen am Ende geöffnet E*istie*x d^ese Datei nicht, wi«d sie angelegt. ios: : app); *1 lf( 1 Stecker ) < *2 —— cerr « "Konnte Datei nicht öffnen!\n”; exi t(1); > ----------— cout « "Bitte eine Ganzzahl: ein » val; Stecker « "Wert:" « setbase(16) « val « endl; *3 Stecker,clöset); "4 “2 Hier findet eine Fchlerübcrprüfung statt, ob der DjteisUom grotfnet werden konnte. Wenn |.i, dann wird das ^ogramm CXit mit dem Fehlercode 1 beendet. j •4 Am Ende ziehen wir ordnungsgemäß den Stecker Jus der Datei. ’3 Den zuv<i? eingegebenen Wert schreiben wir hier mit dem Text Wert: und einem hexadezimalen Wert mit einem Zeilenumbruch in den DaterKtmm Stecker, der md der Datei forrnjbcrt.lrt verbunden ist. her Umgang «)t Strom und 531
lAchtun^l In der Praxis wird das formatierte Schreiben oder Lesen über die Operatoren « brw. >> eher nicht genutzt Aus Effizienz* gründen wird mehr das blockweise Arbeiten im Binärmodus verwendet. Aber dazu kommen wir gleich noch. Das Beispiel bei der Ausführung: $ Femuftww (XMf*-$Lioen Schroedtnger $ ,/formatStrom Bitte ein Ganzzahl: 100 ----- Schrocdinger $ ./formatStrom Bitte ein Ganzzahl: 2600 Schroedinger $ ./formatStrcxn Bitte ein Ganzzahl: 999— Schroedinger $ o Hifr wird Mubf* Formatiert in Jnr 011« ge><hii«b«n Stück für Stück Willst du Byte für Byte lesen und schreiben, dann funktioniert das hiermit recht ähnlich, wie du dies schon vom gewöhnlichen Ein-Musgabestrom mit get () und put ( ) her kennst. Nur musst du natürlich den Strom statt mit ein bzw. cout mit den entsprechenden Dateiströmen anschließen. Eigentlich sollte das jetzt kein Problem mehr für dich darstellen. Der folgende Codeausschnitt zeigt dir, wie du eine Datei zum Lesen Öffnest und alles Byte für Byte in eine andere (gegebenenfalls neue) Datei schreiben kannst: linclude <fstrean» i Zum Offnen wir efoen C-StrinjR "2 Hier findest du sie, die Ströme, efteibeiner zum Einlesen (If Stream: und einer zum Schreiben <of Stream*'. char dateLLesen[255], datelSchreiben(255]; ifstream LeseStroai; *2 ofstream schreibStrom; ’2 S32 FOMfZtHW
cout « "Datei zum Lesen : "; *3 cln.getlinefdateiLesen. 255); *3 leseStrom.open(dateiLesen); *3 cout « "Datei zum Schreiben : *3 cin,getllne(dat.elSchreibent 255); *3 schreibStrom.open(dateiSchreiben); * if( lleseStrotn ) < /* Fehler 111 */ if( 1schreibStrom ) { /* Fehler tll char ch; while(lese$troD.gct(ch)) { *5 if( leseStroo.fallt)) { /* Fehler 3 } M */ > M •3 Die Form.ilrUtet; l*M«i auch nicht auf 5i<h warten Date namen >• nf.cbrn und dann einen entsprechenden Datehtrom damit verbinden III *Z } *6 Natürlich ist es un^rUssHch. e nee Fehler hin m prüfen und zw.ir ob <fci Strom erfolgreu' h zur Dahn Iwrgtttell! werden konnte. Die Fehlerausgdbe und «egcbcnenfilh den P ruß tarn rn abbruch kinnil Hu ja selbe/ implementieren. *5 Hier st.irtm wir in einer whileSchlrifr das byteweise lesen wm Datcistrorr leseSt rom 0«e Schiede liest so lange einzelne Zeichen ein. bis sie auf EOF (» - 1} trifft, womt die Schleife beendet wird schreibStrom.put(ch); ’7 if(schreibScrocufallO) { /* Fehler !!! * > cout « "Kopieren erfolgreich!I!^n leseStrom*close(); ’6 E«oe Überprüfung auf einen Lesefehler hin ist auch hier unerlässlich? “7 Hier schreiben wir das einzelne Zeichen in die Datei, weklie mit dem Strom schreibStrom verbunden ist schreibStrom.close(); ’ H Selbstverständlich wird au<h hier überprüft, ob das Schreiben erfolgreich war IMntiJj Natürlich sollte dir klar sein, dass ein byteweises Kopieren nicht sehr effizient ist Korf* bearbeiten) Hast du eine Textdatei geöffnet und willst du die Ausgabe auf dem Bildschirm über cout realisieren, brauchst du nur den Strom cout statt schreibStrom zu verwenden. ein leseStrom ‘-w-stZ 5c? tiy schreibStrom 5cZ^ Frag nicht! Probier es einfach mal selbst aus! Vergiss nicht, dass du dann EOF mit st'o ♦ ojbzw. sirD ♦ z auslösen musst. U»9<ng Str»l«5 und M3
Zelle für Zeile Es sollte dir jetzt nicht mehr schwerfallen, unsere byteweise Version zum Lesen und Schreiben von und in Dateien zu einer zeilenweisen Version zu machen. |Achlun$| Natürlich sollte dir klar sem, dass ein zeilenweises Abarbeiten eine Datei voraussetzt, welche auch Zeilenumbrüche enthält. So macht es bspw. wenig Sinn, eine ausführbare oder binäre Datei zeilenweise zu kopieren. lEiMachr Aufgabe] Erstelle aus dem eben formulierten Listing, in dem du byteweise eine Datei kopiert hast, eine zeilenweise Version. getlineO ? Genau* Und für das Schreiben kannst du ja den <<-Opcrator verwenden. ff Das Öffnen der Dateien entspricht dem Beispiel ff zuvor und wird daher hier nicht nochmals abgedruckt / "1 Hier Ki nun ein AUMoichcnd großer Puffer, fui dir eine Zerte?! *3 Die elngetesrnrn Zeichen schieben w*r mit << m den DatclsVom schreibeScrom und bangen zudem noch das endl ans Ende *2 Liest mit getline () vom Dalentrom lese Strom maximal sizeof (puffer) Zeirhrn oder bis zum nächsten Zellen - umb/uch e»n. char puffer[4096]; *1 while(leseStroo.getline(puffer, sizeof(puffer))) { *2 if( lescStroo.failO) { /* Fehler !!! *7 } schreibStrotn « puffer « endl; *3 if(schreibstrcra.fallt)} { /* Fehler !!! */ } > cout « "Kopieren erfolgreichI l!\n'*; |Cödr bearbeiten) Das war gar nicht schlecht* Ich will nur noch anmerken. dass du es dir natürlich mit der Klasse String einfacher als mit C-Strings machen könntest. Hier derselbe Ansatz, nur mit der Klasse string string puffer; while(getline(leseStrom, puffer)) { II ... der Rest wie gehabt ... } 634 C4c;t<l fqmfzcmk
Ganze Happen Die wohl beste Möglichkeit der Datenspeicherung dürfte sein, die Daten gleich in ganzen Blöcken zu übertragen. Um eine bestimmte Anzahl an Bytes zu lesen oder zu schreiben, kannst du die ElernentfunktIonen istream:: read() und ostream: :write() verwenden. Auch wenn du im folgenden Listmg nur eine Datei kopierst, so ist diese Methode der Speicherung auch ideal dazu geeignet, Vektoren. Strukturen oder Objekte zu speichern. Hierbei musst du natürlich beachten, dass du den Typ nach char* umwandelst!!! Bezogen auf unser bekanntes Listing> sehen die entscheidenden Zeilen wie folgt aus: 4 lesestronuscckg(0, los::end); ‘1 size_t atze leseStrom.tellgf); *2 leseStrom.seekg(0, ios::beg); ’3 char* puffer new charfsize]; *4 leseStrom.rcad( puffer, size ); *5 *1 Das ist ein wahlfreier Zugriff, mit dem wir den Lesezcigcr auf das Ende der Datei (los: tendi setzen *2 Hiermit bekommen wir das aktuelle Byte-Offset vom Datei anfang zurixk. Wir haben quay die (kö&e der Datei. Den Wen speichern vor in filze *3 Jetzt stehen wir den Lcsezeiger wieder zurück auf den Anfang der Datei. ‘4 Nun kannst du die Anzahl Speicher reservieien. welche wir für d.is komplette Einlesen der Datei benötigen. cout « "Gelesen 2 " « leseStrco.gcount() « endl; *6 schreibStroni.write ( paffert size ); *7 *5 Jetzt lesen wir mit read() die komp?rtti* Datei mit size Byte* in den zuvor reservierten Platz des Arbeitsspeichers "6 Mi! gCOunt ( ) kannst du ermitteln. w»c viel zuvor mit tcad() gelesen wurde. *7 Den kompletten Inh.ft mit Size Bytes aus dem Arbeits- speicher ipuf fer) schreiben wir letzt mit write ( ) in d«e mit dem Dateistrom verbundene Datei (Achtung! Hier wurde der Code gekürzt, um sö auf die Fehlerüberprüfung zu verzichten. Du machst das in der Praxis natürlich nicht solH Dir U»9<ng Streu* ir q P a t ♦ , t M8
Wahlfreier Zugriff Im Beispiel zuvor hast du gesehen, dass du frei bestimmen kannst, auf welche Position du in einem Dateistrom zugreifst. Ebenso kannst du auch die aktuelle Schreib- oder Leseposition ermitteln. Hierzu gibt es jeweils eine pVersion (put; schreibend) und eine g Version (get; lesend). Folgende Elementfunktionen kannst du dazu auf den Dateiströmen verwenden: | Etementfirnktion | Was sic macht... | istrcai»& seckg(long); Lstreanst seekg( long, ios::seekdir); Setzt den Leiezeiger auf eine bestimmte Position des Datei• anfangs oder auf eine relativ zum optional zweiten Argument angegebene eezugsposltion, long tellgO; Gibt dec aktuelle leseposilion zuruck. ostrean£ seekp(long); ostream^ seckp( long, ios::seekdir); Setzt den Schreibieigei auf eine bestimmte Position des Dateianfangs oder auf eine relativ zum optional zweiten Argument angegebene Bezugsposition. long tellpO; Gibt die aktuelle Scbreibposition zurück. (Irmf n'funktiQnrn für wahlfreien DaCvimcriff Jetzt noch schnell die möglichen Werte für das zweite Argument von seekgO bzw. seekpO für die Bezugsposition (vom Typ ios: seekdir): 1 Fhffie | Bedeutung | Los::beg Position relativ zum Dateianfang Los::cur Position relativ zur aktuellen Position Los::end Position relativ zum Dateiende FUfjrn rar 0rrugtp<iiil»c»n Ich weiß, das Thema war recht umfang- reich. und du hast dir jetzt eine ordent- liche Mahlzeit verdient. Nachdem du deine Zigarette fertig geraucht hast, soll- test du überlegen, ob du dir eine Pizza bestellen oder lieber den Dinkelvollkorn- pfannkuchen essen willst, den dir deine Freundin in den Kühlschrank gestellt hat. 538 t<l FüVZCMh
Daten wiederherstellen So, jetzt bist du auch mit den vorhandenen Strömen für Dateien vertraut. Du kennst jetzt die Daieiströme ifstream (zum Lesen). of stream (zum Schreiben) und fstreamfrum l esen und Schreiben). Um einen Strom mit einer Datei zu verbinden, kannst du diesen entweder direkt mit dem Konstruktor oder über die Elcmcntfunktion Open() realisieren. Der Dateiname muss als C-String vorliegen. Um den Dateistront mit einem bestimmten A^odus (jenseits vom Standard) zu öffnen, stehen dir mit dem Typ ios : topenmode einige Flaggen zur Verfügung. INolil) Wenn es dir lästig erscheint, die alten C-Strings als Dateinamen für open() einzulesen, kannst du natürlich auch die Klasse string dafür verwenden. Mithilfe der Elementfunktion c_str() kannst du daraus dann einen für open() lesbaren C-String erstellen. {Einfach# Aulgjbe] Welche Möglichkeiten kennst du. um zu überprüfen, ob ein Dateistrom erfolgreich an eine Datei angeschlossen werden konnte? Sehr gut beantwortet. Schrödinger! Dwr Uwgjng WK Strtm und 537
Ithrfftdit Aulpl>e] Kannst du mir sagen, mit welchen Standardwerten if stream, of stream und f stream geöffnet werden, wenn kein zweites Argument für ios;; openmode verwendet wird? i‘c£_ ifstream *•»// ios:: in, ofstream wZ ios; :out *-*-><* ios::trunc *->>/ fstream Prima! Du hast erfahren, dass du beim Lesen und Schreiben auch alle bereits bekannten Sachen wie den << und den >>Operator, die EJcmentfunklionen get ( ), put() (Byte für Byte). getlineO (Zeile für Zeile), read() öder write() (ganze Datenblöcke) verwenden kannst. Du kannst also fast das gesamte Arsenal der lostream Eleinentfunktionen auch für die Dateiströme verwenden. In der Tat ist dies auf den ersten Blick recht komplex. Ich will es dir anhand eines Objektes demonstrieren, wie du hierbei die Daten speichern kannst. Sieh dir hierzu das folgende Grundgerüst der Klasse SinnloseDaten an: dass SinnloseDaten ( private: string wasauchiccncr; long einWert; float fliessenderWert; public: II ... SinnloseDatenlstring s“’”', long l»0, float f«0) : wasauchimmer (s), einWertU), fliessenderWert(f) () II ... ostreami writc(ostrcam& os); istreami read(istream& is); void putout(); }; 638 c*o>t*i FoarztHw
Das Augenmerk liegt hier natürlich auf der Implementierung der beiden Eie* mentfunktionen writeO und read(). Diese wurden wie folgt implementiert: ostreami SinnloseDaten: :write(ostreatn& os) { *1 os « wasauchinaer « ends; *2 —w os.vrite(reinterpret_cast<char*>(keinWert> , sizeof(einWert)); *3 os.writc(reintcrpret_cast<char*>(&flieascnderWcrt), sizeof(fliesscndcrWcrt)); *3 return os; ‘4 ) istreami SinnloseDaten::read(istreamfe getlinefis, wasauchimmer, ’\0'); ‘6 1s♦read(re in te rpret_cast<char*>(Sei is.read(reinterpret_cast<char*>(SfliessenderWert), sizeof(fllessenderWert)); ’7 is) { *5 irrt)» sixeof (einWert)); *7 return is; *8 > void SinnloseDaten::putout(){ cout « wasauchirmner « endl; cout « einWert « endl; cout « fliessenderWert « endl; > *1 Damit schreibM du die Ditei von SinnloseDaten <a den Strom OS. Zurückgegeben wird der übergebene Strom. *2 Hivr schie^bcn wir den String mit dem Tefrninieru'igwdchen als Manipulator (ends). Achtung, beachte, <k$s es sich bo String auch um ein Tcrtobiekt der Klasse SinnloseDaten handelt. deren Lange immer unterschiedlich sein könnte *4 Zuruckgegeben wird wieder der übergebene $Uom *3 Da write () ein char* als etstes Argument erwartet, musst du hier beide Male in diesen Typ casten. •5 Heim Gegenspieler read i.tuft dies ähnlich ab. nur eben in die andere Richtung read( ) Lest d«e Daten vom Strom is m das aktuelle Objekt ein. r$ Für das Einlesen des Sthngs verwenden wr natürlich auch die string: :gecline( >-€ltmentfwiktion *8 Zurikkgegeben wird der Strom is *7 Auch read () erwartet ein char* aH Argument, weshalb auch hier ein Casting nötig wird. Df Uagjnq Mit und 539
Jetzt brauchst du nur die beiden lileinentfunktiüneti SinnloseDaten: :write() und SinnloseDatcn: : rcad() mit einem passenden Dateistrom zu verbinden. Den Vorgang will ich dir hier ebenfal string dateiSchreiben; '1 ofstream schreibStrom; *1 cout « "Datei zun Schreiben : "; ’1 getllnefcin, dateiSchreiben); *1 schreibStrotn.open(dateiSchrelben.c_str(), Is nicht vorenthalten: *1 Der nötige OateitUom /um binaren Schreiben wird mit einer Datei verbunden, "2 Ein Objekt mit sinnlosen ios:tbinary); ’1 Wetten v,ifd Angelegt lf( lschreibstrom ) ( /* Fehler !!!*/> *1 SlnnloseDat.cn dat (stringfHallo"), 100, dat.write(schreibScrom); *3 schreibstrom.close(); "4 123,567); *2 ifstream LeseStrom; "5 SinnloscDaten dat2; ’6 leseStrönuöpen(daceiSchreiben); *5 dat2. read( LeseStro<n); ’7 dat2«putout(); *8 ’4 Oer Datcvstrom wird heigegcben. *3 Die Daten des Objektes werden jetzt m den Oate»sVom schreibst rom»;cschnebcn Hierbei w?rd unsere Elementtunktion SinnloseDaten::write() aktiv. An dieser Steife wlltcs! du in der Praxis natürlich eine Fehler- Überprüfung einbauen *5 Teilweise verwenden wir jetzt <incn CMtcitfroni zum Lesen aus dewlbcn Datei. In der wir eben geschrieben haben & £tn Icew Objekt. ... A *Ä Das ist der Bewert. d.iss es geklappt hat...’ •7 ... in das wir jetzt die Daten mlthiHc des DateisUoms onlewi Hierzu bedienen wir uns der Etementfunktion SinnloscDaten;:rcad() X (Belohnung! Prim*, Schrödinger! Jetzt hast du wieder ein riesiges Kapitel gemeistert! Zur Belohnung ... Schrödinger? Bist wohl einge5ch laten ...? 540 udiui FwztM*
Mit Blick auf den Anfang des Kapitels, wo ich dir Hierarchie der Stromklassen, ausgehend von 103. aufgczeichnel habe, findest du ganz unten mit istringstream ostringstreamund stringstream drei Ströme für Strings. Alle Klassen sind im Header <SStream> enthalten. Die Ströme für Strings sind hervorragend dazu geeignet, unterschiedliche Datentypen von Strings bzw. in Strings umzuwandeln. Das Prinzip funktioniert recht einfach. Das folgende Beispiel zeigt dir, wie du einfache Datentypen wie int oder double in einen String konvertieren kannst: linclude <$strcara> ostringstream ostring; ’1 string strval; *2 int intval - 999; *3 double douval - 123.456; *3 ostring « intval « " und ” strval - ostring.str(); *5 cout « strval; *6 •1 Hier siehst du einen Suingströcn zum Schreiben ... ’2 ... und hie*das Ziel. ... *3 die ru konvertieren- den Quellen, « douval « endl; *4 ‘4 Damit werden die Textfolgen .939’ und .U3.4%’ in den Strin^sVom OString gesteckt. ’$ Mithilfe der Eremcntfunktlon S tr O wird der komplette Inhalt vom 5tringsUom (genauer vom Stiingputfer) in den String strval kopiert. Am Ende steht schließlich der Beweis.
Natürlich erben die Klassen für die Stringströme sämtliche public-Elementfunktionen von den davor abgeleiteten Klassen, womit dir also auch alle bisher bekannten Elementfunktionen ebenso bei den Stringströmen zur Verfügung stehen. Selbstverständlich funktioniert das auch in die andere Richtung, sogar viel einfacher, als du jetzt vielleicht denkst. Guck dir hierzu folgendes Beispiel an: linclude <sscreaea> *1 Den Slrngstrom benötigen wir zum Konvertieren st rings cream strstr; *1 string strval ”999 und 123.456”; *2 string Platzhalter; *3 int ival; *3 double dval; *3 '2 Die beiden Werte 999 und 123.456 lolknairt dem String gezogen werden. "3 H»cr stehen die notigen Datentypen, in die wir den String umwandein wollen. strstr « strval; *4 //strstr.str(strval); *4 strstr » ival » Platzhalter » dval; ‘5 if( 1 strstr.eof() ) { ’6 cerr « "Fehler beim Konvertiercn^n”; > cout « ival « ” " « dval « endl; ’7 Oer String wwd -n den Stringstrom (bzw. Puffer) gesteckt Dk-ElcHientfujikbofi str(string) wurde dasselbe bewirken. "5 Jetzt werden di* einzelnen Daten Stück für Stück aus dem Stringslrom herauigeichaben und die ASCII- D.irvtellung in eine binäre Darrtellung konvertiert. -e Mithilfe von COf ( ) kannst du überprüfen, ob Fehler beim Konvertieren aufgetreteo sind. Wird eof ( ) be»m String^trom erreicht, ist kein Fehler aufgetrrfen, weshalb wir hier auf ungleich eof ( ) prüfen. "7 Der Beweis, dass die Sthngs m numerische Werte konvertiert wurden, folgt aut dem Fu&e. 542 Ci^.tll FOMFZCMh
Schürzenjäger-Strom Im Büro konntest du schön sehen, dass du mithilfe von Stringströmen ganz bequem Daten von der Hiiiär-Darsiellung in eine lesbare ASCII-Form und umgekehrt konvertieren kannst, Es ist allerdings recht umständlich, auf die Art für jeden Typ eine extra Funktion zu schreiben. Es bietet sich daher geradezu an, hieraus ein Funktions-Template zu erstellen. (Schwierig? Avlgjbrl Erstelle ein Funktions-Template, mit dem du einen Datentyp in einen anderen konvertieren kannst. Der Prototyp des Templates hat wie folgt auszusehen: cemplate <class TI, class T2> void mconvert(const T1& in, T2fc out); ’1 T1 will in den Typ T2 kor .ertlfft werden linclude <sstreaa> •1 Ole Daten gelangen In den Stringstrom hinein ... '2... und konventat aus dem 5lringrtrom wieder heraus. tcmplate <class Tl, dass T2> void mconvert(const TU In, T2i out) { stringstream ss; ss « in; *1 ss » out; *2 if( I ss.eof() ) { *3 cerr « “Fehler bei der Konvertierung\.f."; exit(l); > *3 Natürlich darf eine Ülx*rpiütiing nicht fehlen. hr U»9<ng efrt Streft und Betehen M3
Korf* beirbeitrnj Anstatt das Beispiel mit exit zu beenden, würde ich dir hierfür empfeh- len. eine Ausnahme zu werfen und zu behandeln oder einen bool zurück- zugeben. der Erfolg oder Misserfolg anzeigt. Jetzt noch ein paar Beispiele, welche dieses Funktions- Template in der Praxis testen sollen: string strl«"999"; string str2-"l23.456"; string convstrl, convstr2; int ival; I l. uL 1 e dv.i 1, • 1 Umwandlungen -Ticonvcrt (str l > ival); *1 mconvert(str2, dval); *1 mconvert(ival*2, convstrl); ’1 «<mvert«)v.l*2, convstrZü -1 1 di' da» all« fjektappt hat cout « "int: " « ival « endl; *2 cout « “double: " « dval « endl; *2 cout « "string: " « convstrl « endl; ‘2 cout « "string: “ « convstr2 « endl; ‘2 IBelotaunf] Prima gelöst! Noch ein kurzer Abschnitt, und du darfst dich als Meister aller Strö- me bezeichnen. 544 C«ekt«l FWZtHN
Ohne Isolation Jetzt kennst du mit den Stringströmen noch eine weitere interessante und effektive Technik, um bspw. aus Strings einzelne Dateien oder aus Daten einen String zu erstellen. Aber String* ströme bieten auch noch andere Anwendungsmöglichkeiten an. So konntest du diese auch für Puffe rungszwcckc verwenden - um so bspw. auf eine temporäre Datei zu verzichten. Folgende Stringströme gibt es im Angebot: Strom Mussrichtu: isstringstream Erzeugt einen Stringitrom, jui dem gelesen wer- den kann. Standardmäßig wird als öffnungimodui los : : in verwendet. oatringstrea« Erzeugt einen Stringström, in den geschrieben werden kann. Standardmäßig w»rd als Öffnungs- modus los t : OUt verwendet- stringstream Erzeugt einen Stringsirom, in den gelesen und geschrieben werden kann. Standardmäßig werden als Öftnungsmödi los i t in und los i iOUt verwendet. Verwh»edfne StringUrome Im Angebot R""l] Bei allen drei Konstruktoren kannst du neben einem leeren Kon- struktor als Parameter entweder den Öffnungsmodus eingeben (das sind die gleichen Modi wie beim Dateistrom), einen String (string), mit dem der Strom gleich initialisiert werden soll, oder eine Mischung aus dem String (erstes Argument) und dem Öffnungsmodus (zweites Argument). D*r U«gjng a|t Str®*«» und Dateien. 545
Jep! Ganz genau. So kannst du bspw. einen vorhandenen Stringstrom mit den Modi los : : ate oder los: : app öffnen, wodurch der neue übergebene String ans Ende angehängt wird. Genau genommen sind Stringströme erheblich schneller (da im Hauptspeicher) als langsame temporare Dateien Allerdings muss dir dabei auch klar sein, dass der Arbeitsspeicher bei einem Programmabsturz im Gegensatz zu einer temporären Datei gelöscht wird. Die Verwendung von Stringströmen versus Dateiströmen hängt natürlich auch vom jeweiligen Anwen- dungszweck ab. laclOlMurttl So, Schrödinger! Nun ist es geschafft •konfettischmeiß"! Von nun an kannst du dich als Meister der Strome bezeichnen. Natürlich bekommst du jetzt dein nächstes Abzeichen verliehen. 54t FQMfZtMh
Q E U 7 E M E. 0 C V Hl C Bl Iw 1 in die Standard | Template Library (STL) Ausstechformnnw für Faule Schrödinger wel bereits, wie er eigene Klassenschablonen erstellen kenn, aber dass C++ bereits eine Menge solcher Vorlagen anbietet, wusste er nicht. Darauf haben wir Ihn sicherheitshalber schnell hingewiesen, als er eben wieder dos Rod neu erfinden wollte.
C++ bietet dir viele fertige Kuchenmischungen an. denen du meistens nur noch eine Backzutat (nämlich den Typ) hinzufügen musst. Diese Sammlung von Kuchenmischun- gen wird unter dem Begriff STL (kurz für Standard Template Library) zusammengefasst. Diese STL ist voll und ganz in der C++-Standardbibliothck integriert, und sie würde dir gar nicht mehr unter dem Namen STL auffallen, wenn ich das hier nicht schreiben wür- de. Ein großer Vorteil solcher Fertigbackmischungen ist es auch, dass man dabei keine solche Sauerei hinterlässt, die man wieder saubermachen muss, wenn der Kuchen fer- tig ist. Folgende Vorteile hast du mit Fertigbackmischungen von Dr. STL: Sie bieten ein besseres und stabileres Laufzeitverhallen der Programme. well die Templates Jahrelang von Probanden probiert und von Meisterköchen verfeinert wurden. *** Die Backmischungen sind ein Standard, an den sich jeder andere Kuchen- hersteller halten muss. Daher klappte es bis Jetzt noch immer mit diesen Kuchen, in jeder Küche* Und wenn du mal keinen Plan mehr hast, kannst du bei anderen Gleichgesinnten um Hilfe bei den Rezepten bitten, weil ja jeder auf dem gleichen Stand(ard) is(s)t. 548 SCCMICHN
Die Hauptbestandteile. um einen Fertigkuchen zu backen, sind ein Behälter (ein Container), eine Beschreibung, wie der Kuchen gebacken wird (Algorithmen), und ein Gerät, um den Teig zu kneten oder aus dem Behälter zu holen (Iteratoren}. Objekte verschiedenster Klassen oder z. B. einfache Datentypen. Aber mach dir darum keinen Kopf, ich gehe gleich etwas genauer darauf ein. Deine nächste Frage wird sicherlich lauten, welche Behälter es denn alle gibt?! Aufge- teill werden die Klassen in sequenzielle und assoziative Behälter. Jeder Container ist als Klassen Template geschrieben und kann somit Objekte beliebiger Typen verwalten. Auch um die Speicherverwalrung musst du dich hier nicht mehr kümmern, Sequenzielle Behälter ordnen die einzelnen Objekte linear der Reihe nach und können somit über die Position im Behälter angesprochen werden. Folgende Behälter stehen dir dabei zur Verfügung: I Behälter I I Was er genau ist vector Die Vektoren hast du bereits des Öfteren im Buch verwenden können. Da der Zugriff auf die Elemen- te über eine lnde«numnier erfolgt, ist der Zugriff auf einzelne Clemente sehr schnell. Das Einfügen innerhalb der Elemente hingegen ist recht langsam. Benötigt dre Headerdatei <vector>, deque Ähnlich wie vector, nur handelt es sich hier um eine zweiendige Schlange {double»endrd queueh bei der du am Anfang und am Ende einen einfachen Zugriff auf die Elemente hast. Hierfür benötigst du den Header <deque>. list Die klassische verkettete lineare Liste. Da beim Einfugen einzelne Adressen verwendet werden, ist das Einfügen erheblich schneller als bei vector. Intern wird diese Liste natürlich gleich als doppelt verkettete Liste implementiert. Benötigt die Hca- derdatei SequfnziHlf tCootaww«) Mit std: :array und std::forward^ list wurden in Ct+11 zwei neue Behälter- Klassen eingeführt Bei std::array handelt es sich um ein Array mit fester Länge, was dasselbe Laufzeitverhalten wie em C-Array mit der Schnittstelle von vector anbietet, std: : forward_list hingegen ist, im Gegensatz zu std:: list, eine ein- fach verkettete Liste, die nur vorwärts durch- laufen werden kann, aber dafür schneller berm Einfügen und Entfernen von Elementen ist. Auf die neuen Behälter gehen wir etwas weiter hinten im Buch ein. Aus den sequenziellen Behältern werden die sogenannten Adapterklassen erstellt. Eine solche Adapterklasse enthält einen sequenziellen Behälter als Template-Argument und ist somit Im Grunde ebenfalls ein sequenzieller Behälter. tinfUhrung In tfi« Sfnd«r<| li&r«ry <SH> MB
Folgende Adapterklassen werden daraus konstruiert: I Behälter i Ww er genau Lrt ... " stack Dieses Klassen-Template ist gewöhnlich eine Modifikation von deque und dient da?ur Daten aut einer Seite abzulegen und von derselben Stelle wieder zu entnehmen (wie bei einem Stapel Teiler}. Getreu nach dem UFO-Pdnzip (last In Firrt Outh Hierzu benötigst du den Header <Stack>, queue Ebenfalls eine Modifikation von deque. Dient ah eine Warteschlange nach dem FiFO-Prinzip (First In First Out}, um ein Objekt auf der einen Seile abzu- legen und von der anderen Seite zu entnehmen. Zur Verwendung benötigst du den Header <quCuC>. priority_ queue Wie schon das Klassen-Template queue, nur dass hierbei noch ein extra Priohtätsattribut verwendet werden kann. Somit wird immer das Element mit höchster Priorität von der Schlange entnommen. Das dürfte in etwa dem Prin- zip der prwaten gegenüber den gesetzlichen Kassenpatienten beim Warten in der Arztpraxis entsprechen. Benötigt den Header <qtieue>- AdtptfiidUsffn werden lut den *e<|iifru-iC’lltn Behältern ktmtruiert. Assoziative Behälter hingegen werden gewöhnlich in einer Baumstruktur gespeichert (ist aber nicht so vorgeschrieben), und der Zugriff erfolgt über einen Schlüssel. Folgende assoziative Behälter stehen dir zur Verfügung: 1 Behälter | Was er genau ist... | set Mit diesem Behälter werden die emgefügten Elemente sofort m e»ne sortierte Reihenfolge gebracht. Der Behälter definiert einen immer sortierten Con- tainer, der nur Schlüssel enthält. Ein Schlüssel darf in einem Set nur einmal Vorkommen. Als Headerdatei brauchst du hierfür multifiCt Entspricht fiCt. nur darf es hier mehrere gleiche Schlüssel geben nuip Ahnlkh wie set, nur kannst du hiermit zwei Elemente speichern. Das erste Element ist ein Schlussel, und das zweite darf ein beliebiges Datenelement sein, mit dem der Schlüssel gefunden werden soll. Ein Schlüssel darf hierbei allerdings auch nur einmalig vorhanden sein. Benötigt den Header <map>. multimap Entspricht map, nur darf es hiermit mehrere gleiche Schlüssel geben. bitset Diese Klasse dient dem Abspcichern und Manipulieren von Bit-Folgen. Assoziative flehaller {Container} SSO C.Pit.l SCCHftHH
Behälter allein machen natürlich noch keinen Kuchen! Um ein sicheres Zugreifen auf den Inhalt der Behälter zu gewährleisten, werden Iteratoren verwendet. Iteratoren sind so etwas wie Zeiger, mit deren Hilfe du von einem Element zum nächsten wan- dern kannst. Wie dies intern geschieht, kann dir egal sein, und du wirst es auch wegen der Kontrollabstraktion nicht sehen. Auch Iteratoren allein bringen nix. wenn keine Funktionalitäten da sind. Und daher sind natürlich auch Algorithmen dabei. Die Algo- rithmen arbeiten wiederum mit den Iteratoren, die auf die Behälter zugreifen. Behälter stellen Iteratoren zur Verfügung, und die Algorithmen benutzen diese! Auch neu: In C++11 wurden endlich die Hashtabellen (auch assoziatives Array genannt) hinzugefügt . Da allerdings schon viele Compilerhersteller hier ihre eigenen Süppchen angebolen haben, wurden für die Hashtabellen recht unbequeme Namen wie std::unordered_map. std::unordered_set. std::unordered^tnultimap und std: ;unordered_multiset eingeführt Also alle recht ähnlich wie die ähnlich klingenden Gegenstücke in C++98. Auch die Schnittstelle ist recht ähnlich zu ähnlich klingenden Behältern. Eine solche Hashtabelle ist eine spezielle Datenstruktur aus Schlüssel/Werte-Paaren welche über einen assoziativen Schlüssel adressiert werden. Auf die neuen 0+11- Behälter werden wir etwas weiter hinten Im Buch genauer eingehen. (Inführu^ jn di g Llfrrjry 561
Besser als „Selbermachen" Das Zusammenspiel von Behälter, Iteratoren und ?\lgorithmen will ich dir jetzt anhand ein« selbst erstellten Beispiels, einmal ohne und dann mit STL, demonstrieren. Das wird dir die Sache erheblich deutlicher machen. Zunächst mal die triste Version, in der wir den Iterator und den Algorithmus selber zusammenbasteln: meinlterator suche(meinlterator while( start !“ ende && *start ♦♦start; > return start; } *3 start.meinlterator ende, *3 const doublei dval ) { I- dval ) { (Ff-Pti-r] Du seiltest natürlich hieran noch wissen, dass Iteratoren im Grunde nur Spezial- fälle von Zeigern sind, und deshalb funktioniert eigent- lich alles, was mit Iteratoren in Behäl- tern geht, auch auf Zeiger oder ( ]. *3 Dai iit der Algorithmus, mit dem wir nach einem bestimmten Elemtnl im Ik'fiAltcr suchen- const int n 20; int Behaelter[n|; ‘4 int dval; meinlterator start Behaelter; *5 meinlterator ende Behaelter + n; *5 init(Behaelter, n); *6 Unser .Behälter"’ Hier haben w,r lediglich ein einfaches int-Array mit 20 Elcmenter» verwendet *5 Hier haben wir jeweils CMner» Ittrllöc (Zeiger) auf den Anfang und eirwn auf das Ende des «BehAlterV gevetrt. suchen L Jetzt initialisieren wir >il e Eie mit Wetten Position suche(start, ende, dval); *7 != ende ) { cout « "Sie ein » dval; meinlterator if( position cout « "Gefunden an Pos» " « (position-start) « endl > eise { cout « "Nicht gefunden\n"; > ’7 Der Algorithmus ve«wendet d e beiden Herato<en und weht im BehJltet nach dem Element dval 562 truwi SfCHZtMH
Beim Beispiel konntest du sehr schön das Zusammenspiel von Behälter, Iterator und Algorithmus erkennen. Klar wir haben hier nur ein einfaches int-Array als .Behälter’ verwendet, und es wurde auch noch abhängig vom int-Typ programmiert. Sicherlich kannst du das Beispiel mithilfe der Template-Technik selber .typunabhängl- gcr" machen. Aber genau bei solchen immer wiederkehrenden Aufgaben greift man dann viel lieber auf die Kuchenbackmischung von Dr. STL zurück. Daher will ich dir jetzt dasselbe Beispiel nochmals mit der Fertigbackaischung von Dr. STL erstellen, und du kannst dann auch und Algorithmus verfolgen: linclude <vector> *1 linclude <algorithm> "2 hier wieder sehr schön das Zusammenspiel von Behälter, Iterator *3 der Behälter vector für Integer const int n = 20; int dval; vector<int> Behaelter(n); ’3 for(size_t 1 0; i < Bchaelter.sizef); ++i) { *4 Behaelter.at(i) • !♦!; ‘4 > cout « "Sic suchen: ein » dval; vector<int>::Iterator position = find( '5 Behaelter.begint), Behaelter.end(), dval >; if( position ! Behaelter.endO ) { cout « "Gefunden an Pos. " « {position-Behaelter.begint)) « endl; } eise { cout « "Nicht gefunden\n"; > ‘4 Werte im Behälter ifiilUliüercn. Auch hierfür konnte man noch b$p%v mit generate() einen fertigen Algorithmus verwenden, aber dazu spatei mehr “5 Oer Algorithmus find () arbeitet m>t Iteratoren. Ah erstes Argument crhilt dieser d»<’ Adresse des ersten Elementes und *1$ zweites Argument die Adresse hinter (!) dem letzten Element (also end ()) Zurikkgegeben w^d die Antangsadiesse des ßelundenen Elementes in Form des Iterators position IHötiz] Die Elementfunktion begin() verweist immer auf die Position des ersten Elementes in einem Behälter. Das Gegenstück end ( ) hingegen verweist immer auf die Position hinter dem letzten Objekt im Behälter. Beide Elementfunktionen geben natürlich einen Iterator auf die Position zurück. Einführung In kibr«ry 883
Schön, dass du das so siehst, aber zuvor muss ich noch ein paar Zellen über die Iteratoren verlieren. Du solltest jetzt verstanden haben, dass die Iteratoren der Weg sind, über den du auf die Objekte in einem Behälter zugreifen kannst, zum einen um die Verwendung von diesen bei unterschiedli- chen Behältern zu vereinheitlichen und zum anderen um vor allem die umständliche Verwen- dung von Zeigern als * (ptr+i) ( Element), (ptr+i) ( Adresse) usw. zu ver- stecken (wird auch als Abstraktion bezeichnet). Selbstverständlich wird neben dem Iteratortyp iterator mit const_iterator auch eine Version für konstante Objekte zur Verfügung gestellt. INe'ul Bleibt noch zu erwähnen, dass es unterschiedliche Arten von Iteratoren gibt. So können bspw. bidirektionale Iteratoren mit dem +♦• Operator vorwärts, mit dem —Operator rückwärts und mit * und -> lesend oder schreibend auf die Objekte zugreifen. Dann wäre da noch der Random-Access-Iterator, der alle Funktionen des bidirektionalen Iterators besitzt, aber zusätzlich den (]-Operator überladen hat und auch noch die Addition und Subtraktion bei Iteratoren enthalt, was für Behälterklassen wie vector oder deque benötigt wird. Bidirektionale Iteratoren werden hingegen vom Behälter list verwendet. Neben diesen beiden gibt es noch weitere Arten von Iteratoren. 564
.. und schmeckt auch noch gut! Das war ein kleiner Vorgeschmack auf die fertigen Backmischungcn von Dr. STL. welche dafür da sind, dir das Leben einfacher zu machen. Vielleicht ist es für dich am Anfang etwas verwirrend, dieses komplette Konzept auf Anhieb zu verstehen. Aber ich bin mir sicher, wenn du das ganze Thema durchgear- beitet hast, werden die einzelnen Telle zu einem schönen und deutlichen Gesamtbild zusammengefugt und werden auch dich erleuchten. ItinfKhr Aulga^r] Was sind die Hauptbestandteile, auf denen das STL-Konzept basiert? Prima, Schrödinger! Das Konzept hinier Dr. STl^ leckeren Backmischungcn hast du sehr gut verstanden. ICfarfadie Au<gibe) Hierzu nochmals ein Überblick über die verschiedenen Behälter, die dir zur Verfügung stehen. Ordne bitte anhand der einzelnen Bilder, der Nummer und des Buchstabens darunter den entsprechenden Behälter zu* Die Lösung dazu findest du ganz unten auf der nächsten Seite auf den Kopf gestellt. C l r.f uhryn-j in «J j « llfrriry <$tl> S66
a.> deque b.) list g) vector d.) stack c.) queue f.) priority_queue g.) map multimap h.) set. multiset 56t SCCmZCh*
Definitiv kann ich dir da kein WO'Xdges Rezept ausstellen, weil das natürlich auch vom Anwendungstall abhangt. Aber wenn du in der Praxis viele Elemente einfügen und wie- der löschen willst, solltest du mit dem Behälter list oder dem neuen Behälter f orward_ÜSt sehr gut fahren! fügst du neue Elemente vorwiegend immer an das Ende an, ist ein vector wohl ideal für dich. Für das Anftigen von neuen Ele- menten vorne und hinten eignet sich die deque am besten. Die nötige Verwendung der Adapterklassen Stack, queue und priority_queue spricht für sich. Die assoziativen Behälter, wie bspw. set odei multiset. sind bspw. mindestens genauso effektiv wie list. Aber spätestens wenn cs um dir Suche geht, sind s e t bzw. multiset unschlagbar, weil die Suche in einem binären Baum erheblich schneller vor sich geht als in einer verketteten Liste. Gleiches gilt natürlich auch für map bzw. multimap. nur dass du mit diesen Behältern gleich zwei Elemente auf einmal aufnehmen kannst. Mindest so effektiv wie die klassi- schen Maps und Sets sind hierfür auch die neuen Hashtabellen unordcred_(multi)map und unordered_(mul- ti) set. Während die Zugriffszeit bei den klassischen Maps und Sets noch logarithmisch von der Anzahl der Schlüssel abhängt, ist die Zugriffszeit bei den Hashtabellen konstant. iMetiz] Trotz der allgemeinen Verwendung der einzelnen Behälter hängen die Laufzeiten natürlich auch noch davon ab, wann der beste, durchschnittlichste oder schlechteste Fall eintritt. Aber so tief wollen wir da jetzt nicht graben. Cbnruhrufig In dl« St4ndjr<| llbriry ($TL> 657
Detaillierteres Arbeiten mit sequenziellen Fertigkuchen Nachdem du jetzt ein wenig mit den Fenigkuchen vertraut bist, wollen wir uns die einzelnen Eigenschaften der sequenziellen Behälter etwas näher anschauen. Hierbei zunächst die gute Nachricht: Alle sequenziellen Behälter haben eine Menge gemeinsa- mer Eigenschaften und Elemcntfunktionen. (Hinteff rundinlo) Ich will dir hier nur über die grundlegenden Eigenschaften und Elementfunktroncn der Container berichten. Die STL ist hierbei wirklich sehr umfangreich? Daher empfehle ich dir, 1- " - jJ für weitere Informationen entweder die Referenz deines Com- pilers zu verwenden, oder du guckst dich hierzu auf meiner Lieblingsseite für solche Zwecke um: http://www.cpfaspfuixom/ reference/stl/ ^^B-iBßhäitBjjrstßllen Wenn du einen neuen Behälter erstellen willst, stehen dir dafür unterschiedliche Konstruktoren (inklusive des Kopierkonstruktors) zu Verfügung. *1 Ein leerer deque- . . ,, *2 Damit wird ein vector- BeMIter für Objekte deque<floac> dschlanget 1 8ehalter mit zehn Objekten vwn Typ vom Typ float wird vector<int> einVektorl(10)t *2 int erzeugt. angelegt. vector<Lnt> cinVcktor2(elnVektori); ‘3 vector<int> einVektor3(einVektor1.begint).einVektorl,end()); ’4 EineKlasse cineKlasse; *5 lisc<ElneKlasse> vfeleKlassent 10, eineKlasse ); ‘5 stack<doublc> cinStackl; *6 stack<double> einScack2(einStackl); ‘7 queue<EineKlasse> qKlassenf10); // Fehler 111 ’8 **8 Dai fleht allerdings bei den Adaplerkl.iiien nkht mehr! *7 Natürlich gibt ei auch hier einen KopierkamUuktor *6 Ähnlich funktkwiiert diei auch bei den AdipEefkiaiicn. Hier wird ein leerer Stack fuf Otyrkte vom Typ double er/rugt. 568 ur.t«l SC<nZ(mk
|Z"'«q Die Adapterklassen definieren nur einen Standard- und den Kopierkonstruktor!?! f// Zum Einfügen neuer Elemente in einen sequenziellen Behälter wie vector, deque und list (aber nicht bei den Adapterklassen} stehen dir für jeden Behälter die glei- chen Elementfunkdonen zur Verfügung. Hier die wichtigsten Elementfunktionen im Überblick: 1 Ekmentfunktion | Was sie kann ... 1 void push_back(const T& d); Hingt dis Element d als letztes Objekt in den Behälter in. void push_ftont(const T& d>$ Füg! Element d als erstes Objekt in den Behälter ein. Iterator insert( iterator pos, const T& d); Fügt Element d vor der Position pos ein und gibt diese Position zurück. Iterator insert( iterator pos, sizc_typc n, const T& d), Dito, nur werden hiermit n Objekte eingefügt. iterator insert( iterator pos, InputIterator first, Inputltcrator last)? Damit werden die Elemente von der Positon first bis last kopiert und vor der Position pos eingefugt. Nulrlicbr ElcrnrntfunkliOOWi. um Objrlc'r rinrm ßrhil?rr hinjrujrufüf-rn ’3 Duplizier! den VeCtOT einVektorl mithilfe des Kopierkonstruktors. ’4 Es ist Juch möglkh. eine Teilmenge fine» anderen Behüters zu verwenden. Die Teilmenge wird natürlich über Iteratoren bestimmt. *5 Hier wird ein Behälter vieleKlassen mit zehn Objekten vom Typ EineKlasse r-rzeugt und mit dem Objekt eineKlflSSe aulgdüllt Einführung In di« tt*nd«rö Librjry (SH) 559
Ja und nein! Du kannst in der Tat die einzelnen sequenziellen Behälter (nicht die Adapterklassen) auslauschen. Natürlich musst du hierbei beachten, hei deinem Vorschlag einen Behälter vector<Typ> gegen einen Behälter list<Typ> zu wechseln, damit In list<Typ> nicht, falls du ihn verwendest, der [ ]-Operator verwendet werden kann. Und, nein, es ist nicht egal, was du für einen Behälter verwendest, weil dies vom Anwendungsfall und somit von der Laufzeit abhängt. Aber das hatte Ich ja bereits erwähnt. IHinterxrundinM Die Elementfunktionen push_back() und push_font() für alle Behälter sowie die Elementfunktron insert () für list haben die beste konstante Laufzeit, und daher solltest du diese bevorzugen. Bei den Adaptcrklasscn Stack, queue und priority_queue hingegen gibt es sinngemäß nur eine Elementfunktlon mit push (T& d), um ein neues Objekt am Ende hinzuzufügen. |Abli<eJ Der neue C**11-Standard erlaubt endlich auch eine Initialisie- rung der Standardbehälter über den { }-lnitialsierer. bspw.: vector<int> vec03={1,2,3}; Für den Zugriff auf die Behälterklassen (ausgenommen die Adapterklassen) sieben dir die Elemcntfunktioncn front ( ) für das erste und back( ) für das letzte Element zur Verfügung. Beide Elementfunktionen liefern dir eine Referenz auf das Objekt zu rück. Hei der Behahcrklasse vector hingegen kannst du auch noch den [ ] -Operator und die Elcmentfunktion at ( ) benutzen. Aber das weißt du ja bereits. Bei der Adapterklasse queue hingegen erfolgt der Zugriff auf das erste Element stets über die Elcmentfunktion, und bei den beiden anderen Klassen stack und priority_queue greifst du mit der Elementfunktion top( ) auf das höchste Element bzw, das Element mit der höchsten Priorität zu, seo SCCHZCHW
Du kennst viele der Elemenifunktionen bereits aus den vorangehenden Kapiteln mit vector, aber trotzdem sollten diese hier nicht unerwähnt bleiben: “ size () gibt die länge der vorhandenen Objekte Im Rehälter zurück, * resize ( ) verändert die Länge des Behälters, bspw. wird mit behaelter.resize(10, d) die Länge in behaelter um zehn Objekte von d erhöht. " empty ( ) gibt true zurück, wenn der Behälter leer ist. Ansonsten wird f alse zurückgegeben. Mit max_size ( ) bekommst du die maximal mögliche Länge des Behälters zurück. Der Wert hängt natürlich vom Behälter und der Größe des verwendeten Objektes ab. IZ«U«I] Willst du wissen, wie viel Speicher du für einen Behälter mit bestimmten Objekten verwenden kannst, ohne dass erneut welcher nach reserviert werden muss? Dann solltest du diesen Behälter mit der Elementfunktion capacity () abfragen. Reicht dir diese Größe nicht aus, kannst du diesen Bereich mit reservet) vergrößern. Zum Löschen von Objekten in den Behälterklassen stehen dir mindestens drei Elemenifunktionen zur Verfügung: » p°p_back () entfernt das letzte Element im Behälter. ** erase ( ) entfernt einen Teilbereich ab einer bestimmten Position, bspw. löscht beh. erase (beh. beginf )+5» beh.end()) alle Elemente in beh ab der fünften Position bis zum Ende. » clear ( ) löscht alle Elemente im Behälter. Für die Behälter deque und list steht dir außerdem mit pop_f ront () noch eilte Elementfunktion zur Verfü- gung, um das erste Element im Behälter zu löschen. Zusätzlich gibt cs noch zwei Elementlünktionen, um einen Behälter vorher zu löschen und dann mit einem Inhalt wieder aufzufüllen. So kannst duz. B. mit assignfn, T) den Behälter löschen und mit n Elemente von T ersetzen. Gleiches gilt für assign (pos 1, pos 2) mit dem eben- falls der Inhalt gelöscht und mit Elementen aus dem Iterator-Bereich pos 1 bis pos2 aufgefüllt wird. Bei den Adapterklassen hingegen steht mit pop ( ) nur eine einzige Elementfunktion zur Verfügung, um das oberste oder vorderste Element zu löschen. CinfUhrun^ tn di« T««pi«t« li&r«ry <$H> 561
Tausch mit mir, oder gib mir alle Den Inhalt zweier Behälter kannst du ganz einfach mit der Element funktion Swap () austauschen (gilt allerdings nicht für die Adapterklassen), bspw. bekommt behl mit behl. swap(beh2) alle Elemente von beh2 und beh2 alle von behl Willst du den kompletten Inhalt eines Behälters einem anderen zuweisen, so ist bei allen Behältern dafür auch der Zuweisungsoperator = überladen und kann daher auch ver- wendet werden. Mixen, sortieren und rühren Der Behälter list hat natürlich noch ein paar spezielle Elemenifunktionen. die eben nur bei einer verketteten Liste einen Sinn machen. Da wäre etwa die Element* funktion sott () zum Sortieren einer Liste. Allerdings musst du hierfür den Opera- tor < für den entsprechenden Typ definieren, kh zeige dir das aber in der Praxis noch. Mit reverse ( ) kannst du die Liste komplett umdrehen, womit das erste das letzte und das letzte dann das erste Element wird usw. Für das Mischen von zwei sortierten Listen kannst du die Elementfunktion mer ge ( ) mit list 1 .rnerge (ÜSt2) realisieren. Aber Achtung, der Behälter in list2 ist dann leer!!! Willst du nur eine Teilmenge oder alle Objekte aus einer Liste an einer bestimmten Position eines anderen Listenbehälters hinzufügen, kannst du dazu SpllceO verwenden. |AbU«*] Neu hinzugekommen ist im C-n-11 -Standard eine Move-Seman- tik, mit der sich unnötiges Kopieren von Objekten im Speicher vermeiden lässt, weil damit beim Erzeugen neuer Objekte die Inhalte aus bereits bestehenden Objekten verwendet werden. Bei der bisherigen Copy-Semantik wurde alles komplett kopiert, bspw.: // Neue vec02 = II Alte vec02 - move-S ema nt ik: ntove (vecOl); copy-Semantik: vecOl; SB2 recbteL SC<MZEMW
? O D u 0 O 0 □ Sequenzielle Fertigkuchen ebschmecken 4 *5 Durchlauft den kompletten BeMUef deqElneKlasse und fügt den Integer- Wert (die Klasse enthalt eh nur diesen Wen) dem Behälter vector<int> via push_back() hinnj. Hiermit werden al.e Integer-Werte aus deqElneKlasse nach IvectorOl kopiert. O Okay, im Abschnitt zuvor wurde viel zu den sequenziellen Behältern geschrieben. Hier will ich dir jetzt mal demonstrieren, wie einfach es ist, diese Funktionalitäten in einem Programm zu verwenden. Zunächst will ich dir ein Beispiel mit deque und vector zeigen. Das Beispiel ist natürlich wie immer möglichst einfach gehalten. dass EineKlasse ( *1 int irgendwas; public: EineKlasse( int i = 1 ) : irgendwas(i) -EineKlasse() <} int getValO const { return irgendwas; void setVal(int v) { irgendwas v; ) >1 -1 irwn Behälter werfen wollen 1 Hier ist zunachrt eine einfachste Kl.i1.5e, die wir in {} deque<EineKlassc> deqElneKlasse(5); "2 vector<lnt> IvectorOl; *2 } "2 Hier haben wir einmal einen Behälter deque für EineKlasse mrt fünf leeren Elementen und dann einen leeren vector für integer- "3 IrWtialHiert den Behalte« mit deque<EineKlasse> for(si?.e_t i»0; i < deqElneKlasse.size(); i++) ( ‘3 deqEineKLasBe.at(i) = ±; deqElneKlasse.push_back( EineKlasse(123) )♦ deqElneKlasse.push_front( EineKlasse(456) ) -4 Behälter deque<EineKlasse> jeweils einen Wert hinten und einen wne hinju for(deque<EineKlasse>::eonst_iterator it«deqElneKlasse.begin(); it I» deqElneKlasse.end(); ++it ) { *5 ivectorOUpush_back(it->getVal()); *5 0 for(deque<EineKlasse>::Iterator lt-deqElneKlas$e*begln(); it I“ deqElneKlasse.end(); ++it ) { deqElneKlasse.Insert(itT 2, EineKlasse(99>) break; } > bei dern der Integer-Wert gleich 2 fist, und Jin dieser Stelle (Iterator lt)> werden dann zwei neue Objekte eingefügt < for(deque<EineKlasse>::eonst_iteraror lt«deqEineKlasse.begln(); it ! deqElneKlasse.end(); ++1C ) { cout « £t->getVal() « 1t*; *7 BebAlttr und jibtctefcn lotwlt .ui 583
> cout « endl» deqElneKlasse.clear(); *8 for(vector<int>::const_iterator ic-ivectorOl.begln(); it 1= IvectorOl.endO; ++it ) { <O cout « *it « ’‘7 > cout « endl; deqElneKlasse.cleart); *8 V. Das Programm bei der Ausführung: äM Uhgur Schroedinger $ ,/jeqBehaeUcr 456,0,1,99,2,3.4,123. 456.0,1.2,3,4.123._____ Schroedinger $ _ Die Behälter vnd ihre E ItrnenEtunMiAftM flieh dem Bicken ... An dieser Stelle muss ich noch das Thema Werte-Semantik ansprechen. Und zwar sollst du wissen, dass die Werte in einen Behälter kopiert werden, Das bedeutet, nachträgliche Änderun- gen an den Werten haben keine Auswirkungen mehr auf den Wert im Behälter! Ein Ausschnitt dazu, was gemeint ist: vector<int> ivec; int ival - 1234; ivec.push_back(ival); ival=ival*2; *1 X ’1 Hier wird ival verdoppelt, aber X . dfr.VVrrt im fiefi.iltcr bleib1 rljpirh So. jetzt bin ich dir auch noch ein Beispiel zu den Adapter- klassen schuldig. Hierzu will ich einmal den Behälter stack und einmal prioritiy_queue zum Backen verwenden. 1—1 584 C*cat«l SCCH?Qh
prlority_queue<int> pq; *1 stack<EineKlasse> stackEineKlasse; '1 ’1 WM’r ver»ve*ndffn wir geweilt einen Rrbaltrr prior Lty^ queue for einfache Integer unii den Schalter stack für die Klasse EineKlasse aus dem ßctspicl zuvor pq.push(90); *2 pq.push(80); '2 pq«push(99); *2 while( I pq.eapcyO ) { *3 cout « pq.lopt) « endl; *3 pq-popOi *3 > "3 stackEineKlasse.push( EineKlasse(90)); *2 stackEineKlasse.push( EineKlasse(80)); "2 stackEineKlasse.push( EineKlasse(99)); *2 '2 Mit push( ) Irgvt du die neuen Elrmentr oben auf den (foKMtcr 0*c Elemente Im Stack werden hierbei der Reihenfolge nach abgelegt, wohingegen die Elemente in der priority queue gewahni <h nach aufcteigenden Werten sortiert abgelegt werden. Jedes Element in einem Behälter hat somit leine definierte Ordnung* *3 H»er durchlaufen wir die einzelnen Öeh-iltcf so lange. b«s diese leer (empty ( )) smd Derweil geben wie immer zunächst das oberste Element (top ( >) auf dem Bildschirm aus und entfernen es anschließend (mit pop(J). whilcf I stackEineKlasse,cmpty() ) { *3 cout « stackEineKlasse*top().gecValf) « endl; *3 stackEineKlasse.pop(); '3 > *3 {AcMungl Es ist immer wichtig, dass du bei Ele- mentfunktionen, wie bspw. pop{), vor der Verwendung überprüfst, ob sich überhaupt etwas im Behälter befindet, weil es bei einem pop () auf einen lee- ren Behälter zu einem unvorhersehbaren Ereignis kommen kann/wird. Zum Schluss will Ich dir noch eine extra Backmischung zum Behälter list zeigen, weil dieser Behälter noch einige listens pazifische Funktionen mehr enthalt. Als Typ kannst du natürlich auch hier wieder jeden beliebigen verwenden, aber für das Beispiel will ich nochmals auf unsere Sinnlos*Klasse EineKlasse zurtkkgrtlfen, die ich aber jetzt etwas erwei- tern musste. dass EineKlasse { int irgendwas; public: EineKlasscf int 1 1 ) : irgcndwas(l) {} -EineKlasse() {} int getValf) const { return irgendwas; } void setVal(int v) { irgendwas • v; } friend bool operacor<( const EineKlassei vall» const EincKlasseS va!2 ) { *1 *1 Um die Efomentfonktion list::sort(> zu verwenden. muss der «Dperator für den entsprechenden Typ definiert sein* Einführung jn tfj« St+ndjrd Teaplit» Lifrrjry (SH? MS
if ( va 11. irgendwas < va12.irgendwas > { return true; *1 Um die Elcmentfunlrtiori list::sort() zu verwenden. muss der < C-Operator für return false; den entsprechenden Typ ( definiert sein! > friend bool operator==( const EineKlassei vall, const EineKlasseü val2 ) { if( valt,irgendwas va12,irgendwas ) { *2 return true; , *2 Den “«Operator benötigst du, wenn du else ' dir Elementfunktion list 2 :unique() return false; verwenden willst, um bei einer I sortierten tüte «Ile gleichen aufeinander- folgenden Elemente zu entfernen >; So, jetzt wollen wir den Behälter list mit dieser Klasse verwenden: void ll$te_aus&eben(llst<ElneKla$$e>& 1) { *1 list<EineKlasse>::const_iterator it; for(it-l>begin(); it !- l<end(); ♦♦it ) { cout « it->getVal() « ',*> > cout « endl; } ’1 list<EineKlasse> listeOl, listeOZ, Hste03; llst<ElneKlassc>::Iterator it; int val; fort;;) { *2 cout « "int-Wert eingeben: if( (Kein » val)) || val <- 0 ) break; listeOl,push_back( EineKlasse(val)); liste02,push_back( ElneKlasse(val*2)); > -2 listeOl*sort(); *3 ’1 liste_ausgeben( ) ist eine einfache Funktion, dre nichts anderes nucht. ab einen SehjUer vom Typ list mit dem Sezeichner 1 thilfe eines Iterators zu durchlaufen, um den Inhalt auf dem 8ildfdtirm auuugeben. *2 Fugt beliebig viele Werte beim Einlesen an das Ende von listeOlundliste02 mit de/ Etementfunktion push_back() an Abbruchbedingung ilt der Wert 0 fn listeOl wird der clngeiesene Wert ange- hangt, und in listeÖ2 w»rd dieser Wert verdoppelt und angetan# ’3 Hiermit sortieren wir beide flehllter .luhtrigrnd nach deren Werten H erzu tsl es allerdings nötig, dass de* <-Operator für den Typ implementiert ist. was In unserem Fall ja In der Klasse EineKlasse gemacht wwde. Che Ausgabe bestallt, dass die beiden Behälter ordentlich sortiert winden. 566 t.cmi SC<h?(hh
Iiste02»sort(); *3 llste_ausgeben(Mste01); *3 liste_ausgeben(listeOZ); ’3 ÜsteOl ,merg.e(üsce02); *4 üste__ausgeben( listeöl); '4 *4 Damit mischen wk die be»den Listen listcOl und li$tc02 zusammen. Wenn die Listen soit»ert waren, hast du in ÜsteOl eine neue sortierte Liste mit den ehemaligen Elementen aus liste02 Der Behälter llsteOZ ist allerdings anschließend leer IM ÜsteOl»unique(); ”5 liste_ausgeben( ÜsteOl); *5 üst<EineKlasse>:: iterator itOi; fordt-listcOI .bcglnO; it I- ÜsteOl ,cnd(); ++it ) { if(it->getVal() > 100) { *6 liste02.splice( liste02,begin()» ÜsteOl, ’6 ic, ÜsteOl >end()); •5 Damit entfernst du gleiche aufeiiunderiölgende Elemente im Behälter. Hierfür muss allerdings -auch der =— Operator für den Typ implementiert sein, was wir ja für EineKlasse ebenfalls gemacht haben. break; } } liste_ausgeben( HsteOZ); '6 ß Hier durchlaufen wir zunächst mit dem iterator dm Behälter llsteO l. tns wir e»n Element finden, bei dem der Wed £ inK lasse::! rgendwas großer als iOO «st Alle Wene ab dieser Püsiton (großer aK 100) fit) bis zum Ende iüsteOl. end( )) fügen wi mithilfe von splicc () am Anfang (listc02 .begin( )> des Behälters vom Behälter ÜsteOl ein, wie die anschließende Ausgabe auch beweisen soll. Das Programm bei der Ausführung: Schroedinger S ./aeineListe lot-Bert eiogebem 123 int-Bert eiA^ben: 1? tn^-Bert eingrben; 1? wt-Bert emgeben: 234 int-Bcrt emgebm: 23 int-Bert eingeben: 23 int-Bert eingeben: 111 int-Bert eingeben: H LZ.12.23,23,Hl, 123,234. 24,24,4* ,44,222,24fc.4b«, 12.12.23.23.24,24^.46.111.123.222.234,246,^. *.**0^ 12.23.24.4«.111.123.222.234.246,46«. 111.123.222.234.246.46«. Air*#. 4t Schroedinger $ Zwei Elementfunktionen hatte ich da noch für dich. Da wäre zum einen reroove (val). womit du den Wert val aus der Liste löschen kannst, und reverse( ), womit du die Reihenfolge der Liste komplett umdrehen kannst. CinfUhrun^ in Qi. Sfnfl.ra libr.ry ($Tl> 867
Bereit zum Essen EineKlasse Jetzt bist du eigentlich ziemlich gut mit den sequenziellen Behältern vector. deque und list und deren Adapter- klassen stack, queue und priority_queue vertraut. sort() Die Frage könntest du dir eigentlich selbst beantworten. Probier es einfach aus! Aber ja. du hast Recht! Verwendest du eine Liste mit einem Basisdatentyp wie liSt< it > oder ähnlich, brauchst du dich um nichts mehr zu kümmern. Für die Basisdatentypen übernimmt der Compiler für dich diese Arbeit, und diese Operatoren sind fix und fertig implementiert. Bei einer Backmischung für list< > hingegen musst du eben entsprechende Vergleichsoperatoren selber schreiben. Und wo wir schon dabei sind: Dasselbe gilt für alle Behälter, wenn du die Vergleichs- operatoren wie ==. !=<,>. <s oder >— verwenden willst. In Verbindung mit den Basisdatentypen kannst du diese Vergleichsoperatoren alle mit den Behältern ver- wenden, wie du willst, um ganze Behälter miteinander zu vergleichen. Aber bei eige- nen Typen musst du auch hierfür Vorkehrungen treffen. Für einen Vergleich zweier Behälter mit == oder I muss mindestens der -Operator für den entsprechenden Typ implementiert sein. Für die anderen Vergleichsoperatoren <. >. <= und >— musst du mindestens den <-Operator für den Typ definieren. 568 sc<M?tMW
list<EineKlasse> listeOl, HstcOZ; lf( listeOl I- HsteOZ ) *1 cout « "listeOl ! liste02\n" lf( liste02 > listeOl cout « "llsteOZ > listeOl\n"; *1 Nur dank definiertem == OperatQ« möglich’ *2 Und die$ geht nur, wenn der <«Opef4tor implcnic-nlicTt wurde Ja, da hast du natürlich Recht! Das Grundlagenprinzip basten durchaus darauf dass die Algorithmen mit unterschiedlichen Behältern verwendet werden können. Aber einige Behälter implementieren auch bestimmte Algorithmen selbst. Im Fall von sott () gibt es aber neben list: i SOtrt () ebenso einen globalen Algorithmus std : : sott ( ) zum Sortieren. i (finLachr Aulgib*] Welcher Fehler wurde im folgenden Codeausschnitt gemacht? Und wie kannst du einen solchen Fehler vermeiden? scack<int> meinScack; meinStack.push( 100); II - meinStack.pop(); II - meinStack,popO; emptyO. [(Mahnung] Perfekt!!! Ich denke, du bist mit den sequenziellen Behältern gut gefahren und solltest dich gleich im nächsten Kapitel an die assoziativen Behälter heranwagen. Bis dahin solltest du dir mal wieder etwas Entspannung gönnen. Wie wäre es mal damit, auf dem Sofa zu liegen und deine MP3-Sammlung durchzuhören? CinfUhrung In di« $t«nd*rd Lit>r«ry <$TL> 689
Neben den sequentiellen Behältern gibt es noch, wie du ja bereits erfahren hast, assoziative Behälter, die nicht linear angeordnet sind. Ganz besonders wenn du nach einem Element suchen willst, bist du mit einem assoziativen Behälter wesentlich schneller unterwegs. Bei einer linearen Suche wie innerhalb einer Liste musst du im schlechtesten Fall alle Elemente durchlaufen. Bei einem assoziativen Behälter bist du immer schneller unterwegs, weil gewöhnlich ein binärer Baum mit minimaler Höhe verwendet wird. Standardmäßig erfolgt die Sortierung bei den assoziativen Behältern wie set, mutliset und map. raultimap. angefangen beim kleinsten Schlüssel. 570 scchjchw
iHinCrrgrundintal Natürlich sollte dabei auch erwähnt werden, dass eine bessere Performance deiner Anwendung erst bei einer umfangreicheren Datenansammlung bemerkbar wird. Für kleinere Daten- ansammlungen bist du nach wie vor mit sequenziellen Behältern gut beraten. In einem SCt enthält jedes Element einen .eingebetteten" Schlüssel. Genau genommen ist das Element (bzw. sind dessen Daten) selbst der Schlüssel. Im Fall eines Vergleiches musst du daher auch das Element mithilfe des Schlüssels implementieren, Im Gegensatz zu set kann der Behälter multiset auch mehrere Elemente mit dem gleichen Schlüssel enthalten. tAbUg«] Weil der Schlüssel ein Teil des Elementes ist, musst du im Fall einer Klasse immer den «-Operator definieren. Der Behälter map hingegen enthält einen echten Schlüssel und ein Element! Es ist quasi ein Schlüssel/ Objekt-Paar. Der Schlüssel wird dabei extra gespeichert und Ist nicht im Objekt .eingebettet'. Auch hier erfolgt ein Vergleich wieder über den Schlüssel. Da map nur eindeutige Schlüssel sichern kann, steht dir mit multimap ein Behälter zur Verfügung, welcher auch mehrere gleiche Schlüssel sichern kann. „.......... , „ , , , , , *1 Der SUMardkonsVuktof legt Für beide Container gibt es neben dem Standardkonstniktor ein sct (&d<, multiset) noch einen zweiten, mit dem du Elemente aus einem (Teil*) mit der Lange o an. Bereich von Iteratoren In das neue set bzw. multiset ein- *2 Entern einen fügen kannst. mu * ^maP Behälter m dem kompletten Inhalt des aet<EineKlasse> setOl; ’1 Behälters listeOl multiset<E£ncKlasse> sct02(liste01.bcgin(), ÜsteOl.end()); *2 Das Einfügen neuer Elemente in den Behälter erfolgt ganz einfach mit der Elementfunktion insert (). Allerdings musst du dabei beachten, dass bei einem set nur dann das Element hinzugefiigt wird, wenn es nicht existiert. Aber du kannst bei Bedarf ja auf multiset ausweichen. Zum Entfernen eines Elementes aus der Liste kannst du aut’ erase ( ) zurückgreifen. Hierbei kannst du ent- weder das Element selbst, seine Position oder einen (Teil-)Bereich angeben, der gelöscht werden soll. Natür- lich steht dir mit clear () auch hier eine Möglichkeit zur Verfügung, den kompletten Container zu leeren. Zum Suchen einzelner Elemente bietet sich die Elementfunktion f ind(T) an. Mit der Elementfunktion COunt (T) kannst du ermitteln, wie viele gleiche Elemente es gibt. Bei einem set kann dieser Wert nur Ooder 1 sein. Bel einemmultiset hingegen kann der Wert größer als t sein. Cl^fUhrun^ In di« ?«»pl«t« Li&rjry 571
Die Konstruktoren und Eletnenifunktionen des Behälters sind dieselben wie schon zuvor bei set und rnult iset. Nur ist das Prinzip hierbei etwas anders, weil diese Behälter ein Paar von einem sortier- baren Schlüssel und ein Element darstellen. Damit das auch klappt, wird das Klassen-Template pair<const Key , Typ> definiert. Hierbei kannst du mit position->f irs t aufden Schlüssel und mit pos ltion->second auf das entsprechende Element zugreifen. Eine Besonderheit haben diese Behälter doch noch: tm Gegensatz zu den set-Behältern ist hier der [ ) -Operator implementiert, und zwar so. dass du hiermit über den Schlüssel auf das Element zugreifen kannst. So kannst du dir das vorstellen: map<string, long> plz; map03("Frankfurt") -50000: map03("Augsburg"]-86153; Der Behälter bltset wird zum Sichern von Zum Beispiel: bitset<1000> bitteelnBit; * *1 bitset<8> nocheinBit(3); *2 string blttcclnStringC 10101010"); *3 Bit-Folgen verwendet. Im Gegensatz zu den üblichen Bit-Manipulationen, die man In C++ mit den Basls- datentypen und den Bit-Operatoren machen kann, bist du bei diesem Behälter nicht an einen Datentyp und vor allem nicht an eine Länge gebunden. bitset<8> strlng2bic(bitteeinString); *3 bitset<4> extractBit(bitteelnString, 4,4); “4 •1 Erzeugt ein heues bit set-Obje+t mit 1.000 Bitv, die jlle auf 0 gesetzt sind *2 Erzeugt ein rwuci Objekt mit ß ßitv welches m.t 3 (00000011> InltiaWrrt mirdc *3 Wdrxtelt einen 5<6ng in ein bitSCt um •4 LOvt die höchsten vier Bits un SUing heraus und fugt 51c zum bltset hinzu. Die Ein- und Ausgabe von diesem Behälter erfolgt ganz einfach über ein und COUt. Natürlich immer vor ausgesetzt. die Kombination besteht aus 0 und/oder 1. Für die Manipulation der einzelnen Bits stehen dir auch sehr nützliche Funktionen zur Verfügung. Mit set ( ) ohne Argumente kannst du alle Bits Im Behäl- ter auf 1 setzen. Mit set (n) kannst du auch nur das Bit n auf 1 setzen. Dasselbe kannst du mit dem Gegen- stück reset() bzw. reset(n) machen, um alle oder ein bestimmtes Bit im Behälter auf 0 zu setzen. Zum Invertieren von allen oder bestimmten Bits gibt es die Methode flip () bzw. flip(n). tergrun4info| Natürlich lassen sich auch die klassischen Bit-Operatoren auf diesen Behältern einsetzen. Alle Operatoren sind hier entweder global überladen oder als Elementfunktion implementiert. So kannst du bspw. zum Verschie- ben alter Bits nach links den Operator « oder nach rechts » verwenden. Zum Umwandeln von einem bitset in einen String oder einen long Wert stehen dir auch mit to_string und to_long Elementfunktionen zur Verfügung. 572 c*okt*i scchzchr
Assoziative Kuchen backen Nun haben wir genug geredet, und sicherlich willst dujelzi mal wieder was in deinen Editor Uppen und was backen. Für unser Beispiel verwenden wir nach wie vor die Klasse EineKlasse. welche du noch vom Kochkurs mit den sequenziellen Behäl- tern her kennen solltest. Natürlich kannst du auch normale Datentypen und andere Klassen als Zutaten für deinen Kuchen verwenden. Aber das weißt du ja jetzt bereits. Also mach dich Jetzt bereit für ein sehr praxisintensives Kapitel. Hier nochmals die alte Klasse EineKlasse: EineKlasse { irgendwas; 1 ) : irgendwas!i) dass int public: EineKlasse! int i -EineKlasse!) {} int getVal!) const ! void setVal(inc v) friend bool opcrator<( const EincKlassek vall, const EineKlasseS val2 > if! val1.irgendwas < val2.irgendwas ) { return } eise { return ) true; false; return irgendwas; irgendwas “ v; ) '1 Den Operator solltest du implementiert Mben für eigene Typen, sonst verweigert dir dem Compiler die Übersetzung. Dieser Oper.itor wird r>6liß. damit de* Schlussel sortiert (multi)set me up, babyl cingcfugt werden kann! Bei den Basisdatentypen brauchst du da natürlich n«<hu mehr zu machen. > >; < O Zunächst werden wir uns den Behältern set und multiset zuwenden und dort unsere Zutaten reintun. Bei diesen Behältern ist Ja das Element selbst der „eingebettete" Schlüssel! Im Fall der Klasse EineKlasse wäre somit der Wert irgendwas der Schlüssel. Die meisten Klassen werden natürlich mehrere Typen speichern, und da musst du dich dann entscheiden, was du als .eingebetteten* Schlüssel verwenden willst. Entsprechend musst du dann natürlich den <-Operator implementieren. Cinführun^ in di« St^nd«rd lltkrjry (SH) 573
Nun das Beispiel zu (multi)set: void set_ausgeben(set<EineKlasse>4! 1) { *1 set<EineKlasse>::const_iterator it; for(it-l.bcgln(); 1t ! l.endf); ++it ) { cout « it->gecVal() « } cout « endl; > *1 void mulciset_ausgeben(multlsec<EineKlasse>!i l) multiset<EineKlaase>::const_iterator it; for(it-l.bcgin(); it ! l.end(); **it ) { cout « it->getVal() « 1 > cout « endl; > < *1 '1 Hie«-stehen einfache Hilfsfunktionen, welche den Inhalt eines «et- bzw multisct-Beh£lte<s durchlaufen und ausgebefk list<EineKlasse> ÜsteOl; set<EineKlasse> setöl; $et<EineKlasse>::iterator it; int val; *2 In der Sch'eite lesen wir wieder Werte für unser Objekt ein, hangen dieses an d.n Ende-ein« 1 ist-Behüten und for(;;) { cout « ”int-Wert eingeben: if( (Kein » val)) || val < 0 ) break; *3 fügen denselben Wert auch dem sct-Behaiter sortiert hinzu. Dank des definierten <-Operators kannst du dir skhe» sc«n. dass das Objekt mit dem .eingebetteten* Schlüssel jetzt sortiert eingelügt wurde. ÜsteOl ,push_back( EineKlasse(val)); B2 setOlünsert( EineKlasse(val)); *3 } multiset<EincKlasse> set02(listeO1.begin(}> liste01.end());"4 set_ausgeben(set01) ; ’5 *5 Dir Ausgabe bestätigt Tnulti$ct_avsgcbcn(set02); *5 das Ganze1 *4 Hier zeige ich dir, wie du die Zutaten aus dem ÜSt-ßehäJler komplett enem multiset-Behüter hirvufugen kannst. □.is Beste d.v.in ist. dass die einzelnen Objekte anhand des .eingebetteten- Schlüssel* sortiert werden. cout « "int-Wert zum Löschen eingeben: lf( (Kein » val)) || val <- 0 ) *6 return -1; *6 it set01.find( EincKlasse(val)); ’6 lf( it !• setOl.endt)) *7 setOl.erase(it); ’7 $et_Hu$gebcn(sct01); "8 ’6 *6 Liest einen Wert zum löschen ein und sucht mithdfe von set:: find () ’7 Wenn der Iterator it be< der Suche nicht auf end ( ) im öehauer zeigt, haben wir das zum Löschen gewünschte Element gefunden und entfernen dieses mittels erase () aus dem fleha ier #74 SCCHZCM* ’S Das ist der Beweis der Löschung!
Das Programm bei der Ausführung: set und mu*tort b*i df* Arbeit Now (multi)map me! Ein ähnliches Beispiel will ich dir Jetzt auch mit {mult imap zeigen. Hierbei hast du Ja mit pair<key, Typ> zwei Elemente, die du im Behälter speichern kannst. Wobei das erste Element immer der Schlüssel Ist. Sollte es sich also beim ersten Argument um einen eigenen Datentyp handeln, musst du auch hier den <-Operator dafür definieren. Bei Basisdatentypen oder Klassen, die diesen Operator bereits implementiert haben (bspw. string). brauchst du nichts zu unternehmen. Jetzt das Beispiel zu (multi)tnap: void multimap_ausgeben(multimap<int, EineKlasse>4 1) ( *1 multimap<int, EincKlassO: :const_itcrator it; cout « "multimap: for(it-l.begin(); it 1« l.endO; ++it ) { cout « ic->first « « l.count(lt->first) ‘1 « •)» « • / •; cout « it»>sccond.getVal() « ", *1 > cout « endl; } void multimap_ausgeben2(niap<EineKlasse, int>4 1) { *1 map<EineKlasse, lnt>::conat_iterator it; cout « "map: f or (it-1 ,begin(); it 1« l.endO; ♦♦it ) { cout « it->first.getValO « '/•; *1 CfcnfUhrurg In dl« St4r>djr<| TopHti Lfcbriry 575
cout « it->second « ", *1 } cout « endl; > multimap<int, EineKlasse> mapOl; *2 tnap<EineK Lasse, int> map02; *2 int val; *1 Hier st«hrn einfach« HiHtfunktronen. um den Inhalt eines imult 1 niap-Öeh.iilers auvugeben. Wie bereits erwähnt, kannst du mit first auf den Schussd und mii second auf die Daten iu^fen wt count (> geben wir außerdem tw« multimap in den Klammern mit aus, wie viele gleiche Schlüssel gespeichert wurden. nun findest du einen mulcimap-Behälter mit int als Schlüssel und EineKlasse als Datenelement und den anderen nxap-BehJiter nvi EineKlasse als Schlüssel und int forlint >1; i--i ;♦♦!) { cout « ”int-Wert eingebent if( (l(cin » val)) || val < 0 ) break; als Datenelement. Oer map-Bch Alter muss natürlich den <<Qpeialor für EineKlasse implementiert haben. “3 In einer Schleife fügen wir mi: insert() mapOl.insert(pair<int, EineKlasse>(i.EineKlasse(val)));"3 jeweils ein entsprechendes rnapO2 + insert (palr<ElneKlasse, inc>( EineKlasse (val)>i));-3 > mapOl,insert(pair<int, EincKlassc>(1. EincKlassc(100))); ’4 Schlüsseb/Datenpaar in den Behälter mit ein« f5 wird wh dem Schlussel sortiert eingefugt. multimap_ausgeben(map01); ’5 mult iniap_ausgcbcn2 (map02); *5 ‘4 Hier fugen wir mit Abucht ein weiteres Schlussel 7Datenpaar mit e»ncm bereits vorhandenen Schlüssel hinzu. Das Programm bei der Ausführung: M^r*h Schroedinger $ ,/mop*»e int-Hert eingcbcn: 33 int-Wort eingeben; 11 mt-Hert einpeben: 22 int'Bcrt eingebcn: 11 int-Reet etngeben; 0 multimap: 1(2)733, 1(2)7100. 2(1)711. 3(1)722. 4(1)/11. mop: 11/2, 22/3, 33/1. Schroedlnger S '5 Die Ausgabe zeigt seh* schön, dass der mult imap-ßehaitev in der Lage ist. durchaus zwei gleiche Schlüssel tu speichern. Der Behälter map hingegen verwendet zwar das Objekt EineKlasse ak $<hluwl. kann aber keine doppelten Schlüssel speichern, map uod multimap bei der Arbeit Bitte ein Bit! Zu dem Behälter bitset muss man eigentlich nicht allzu viel sagen. Wenn du etwas für eine Bit-Manipulation brauchst, bist du mit dieser Backmischung gut beraten. Für solche Zwecke will ich dir natürlich auch noch ein Listing tnil den wichtigsten Funktio- nen dafür demonstrieren. 576 c*oit»i sccMttMw
Hier ein paar Bit-Spielereien ... bitset<8> CinBit; *1 cout « "Bitte gib mir die Bits: "; if<1 (ein » cinBit) ) *2 return -1; cout « "Deine Eingabe: " « cinBit « endl *1 Wir verwenden einen bitset-Behälter für 8 Bits. CinBit.flipO; ‘4 cout « "flipO : " « cinBit « endl cinBit «- i; *5 cout « "« 1 : " « cinBit « endl; cinBit »- 1; *5 cout « "» I : cinBit.flipO ; *4 « cinBit « endl; "3 Auch CÖUt kannst du hier wie üblich für die Ausgabe dn bitset Behalten verwenden "4 Damit wird jedes Oflit Jul 1 und jedes 1 -Bit auf 0 gesetzt. *2 Einfaches Einlesen von Nulten und Einsen von ein Etngelcscn werden maximal acht Nullen oder Einsen oder b<s zum ersten Zeichen das, was keine 0 oder 1 ist! Werden weniger als 8 Bits eingegeben, so s<nd dm eingege- ben Bits immer die niederwertigen Bits, von rechts nach links gelesen Wird als erster Wert keine 1 oder 0 emgegeben. bricht ein mit einem Fehler ab *S Damit werden alte Bits um eine Stelle nach links bzw. rechts verschoben. Bits, dk aus dem 0-it-FeldbcfCxh geschoben werden, werden verworfen string bit2string cinBit.to_string<char, char_traits<char>, allocator<char> >O; *6 cout « "Als String: " « bit2string « endl; cout « "Als unsigned long : * << cinBit.to_ulong() << endl; *7 cinBit.setO; ’8 cout « "setO : " « cinBit « endl; cinBit. resetO ; *9 cout « "resctO : « cinBit « endl; CinBit.set(5); MO cinBit(6) = 1; ‘10 cout « "set(5)/cinBit(6j*l : " « cinBit cinBit.flip(5); ‘11 cout « "flip(5) : " « cinBit « endl; cinBit.resec(6); *12 cout « "reset(6) : " « cinBit « endl; *6 Dieser wus! B .jussehende Code (em Eunktions-Template) •7 Den unsigned long-Wert aus der flil-Darstellung kannst du dir mit to_long( ) zuiuckgeben lassen. macht aus dem Behälter «inen String Leider musst du den so verwenden! « endl; B Verwende*! du SCt ( ) ohne Aigumenle, werden alle Bits im Behälter auf 1 gesetzt '9 Eui res et ( ) ohne Argumente j "10 Einzelne Bits kannst du entweder m4 SCt und der Position oder ganz einfach mit dem Indexoperator setzen. [Notdz) Willst du wissen, wie viele Bits der Behälter enthält, kannst du die Elementfunktion size() verwenden. Wie viele Bits im Behälter auf 1 gesetzt sind, erfährst du mit count(>, und ob überhaupt eines gesetzt ist. kannst du mit any () ermit- teln. any() gibt true zurück, wenn mindestens 1 Bit im Behälter gleich 1 ist. "12 Em I.Bit auf 0 setzen, kannst du auch mit reset ( ) und der Pö5*t»o^M^gab<, Alternativ kannst du auch hier den indeKoperator verwen- den und 0 übergeben. "11 Auch ein e nzefnes Bn kannst du unter Angabe der Portion mit f llp( ) invertieren. Einführung in Ui« Standard Teaplat« Library 577
Die Bit-Spielerei bei der Ausführung: Ä fcotMfea 1 Schroedinger $ ./ploytrxjbit Bitte gib mr die Bits: 10101010 Deine Eingabe: 13101010 Hip<) : 31010101 «- 1 : 10101010 »- 1 ; 01010101 Als String: 10101010 Als un&igned long : 170 set() : 11111111 resetf) : 00000000 set($)/cinBit[6].l : 01100000 flip<5) : 0100000© rcsct(fc) ; 00000000 Schroedinger $ Hier spotten wir mit dem b £ t S C Ü-Br hjllrr 678 c^cLtel SCCmZCmh o.
Auch assoziative Kuchen kenn men essen Nachdem wir in der Werkstatt jetzt wirklich ziemlich viel gebacken haben, wollen wir es am Ende mit den assoziativen Behältern etwas ruhiger angehen lassen. Bevor du mich Jetzt wieder unterbrichst, frage ich Heber gleich, ob du noch ein paar Fragen hast? Natürlich, das ist sehr einfach, guck dir folgenden Code an: map<stringt long> plz; *1 string suche; plz("Frank£urt"]«6O31h ‘2 plz("Augsburg")=86153; ’2 plz["Bonn"|-531lI; ’2 cout « "Welcher Ort: "; ein » suche; map<string, long>::Iterator if( it I- plz.endO ){ "3 *1 Wir verwenden ak cm String Objekt und ak Wert den Typ long *2 letzt kannst du g.w einfach den Schlussel innerhalb dos Indexoperators verwenden. ‘3 Hier rteht eine einfache Suche nach einer Postleitzahl für it plz.find(suche); *3 einen bestimmten Ort, cout « "Die Postleitzahl dazu lautet: ” « plz[suche] « endl; *4 > eise { cout « "Konnte keine Postleitzahl für ” « suche « finden\n"; ’4 Auch für die Ausgabe auf dem eikfcchtfm lasst sich jetzt das String-Objekt innerhalb des Indexoperators verwenden. Anstatt plz [ suche ] würde ich dir in der Pr.t« •. it->SCCOnd empfehlen. Aber ech wollte dir nur demonstneren. dass es r»bcn .w“ auch geht (Eimfacht Aulgibr] Bitte sag mir doch, ohne ein Programm auszuführen,. wie die entsprechende Bit- Darstellung jeweils bei cout aussieht? Die Lösung findest du neben der Aufgabe auf den Kopf gestellt! Einführung Lr> diu Stihdirj Teaplutu Librury 579
string bitsC’IOIOlOlO"); bitset<8> bset(bits); bset « 7; cout « bset « endl; 'S bset« fllpO; cout « bset « endl; ’b bset(7]«0; cout « bset « endl; *c bset.flip(O); cout « bset « endl; 'd cout « bset.to_ulong() « endl; *e bitset /vZ vector<bool>/ 921 (o OLLllLLOlP LLllLLLl <q 00000000 (e ßunsQi Das hast du aber recht gut erkannt! Im Grunde hast du ja Recht, aber im Gegensatz 211 vector<bool> kannst du ein bitset nicht mehr nachträglich ändern, weil die Anzahl der Bits als kons tanier Integer angegeben werden muss! Folgendes ist also nicht möglich: int val; ein » val; bitset<val> mchrbits; '1 •1 Fehler*!! Die Anzahl der 8<ts muss als Integer- Könstanie angegeben werden. Und im Gegensatz zu einem vector<bool> ist ein bitset kein sequenzieller Behälter, sondern ein assoziativer Behälter, und ein bitset hat keine Iteratoren! Das bitset ist also eher rein Ihr pure Bit-Manipulationen gedacht. IBe-loMungl Die assoziativen Behälter durften dir auch keine allzu großen Probleme mehr bereitet haben. Bevor du dich zum Finale auf die Algorithmen der STL stürzt, würde ich dir eine Pause emp- fehlen. Wie wäre e$ mal wieder mit einer Runde WoW? 560 SCCMZCMH
Wenn du spezielle Funktionen, oder genauer Algorithmen. Tür deine Behälter brauchst, wirst du bei Dr. STL auch nicht sitzen gelassen und musst dich nicht selber darum kümmern. Denn auch hierfür bietet dir Dr. STI. eine Menge (oller Algorithmen an. Ein Algorithmus ist eine Vorgehensweise zum Lösen eines Problems in endlich vielen oder genau beschriebenen Schritten. Dabei enthalten sind eine Menge Algorithmen zum Suchen. Sortieren. Kopieren. Vergleichen. Zählen, Ersetzen, Löschen, Permutieren oder Umwandeln in einen Heap. Das Tolle an den Algorithmen ist. dass sie ziemlich flexibel für alle Arten von Behäl- tern. aber auch von gewöhnlichen Basisdatentypen verwendet werden können. Das liegt daran, dass diese als Template-Funktion implementiert wurden. Um Algorithmen zu verwenden, musst du den Header <algorithm> im Programm einbinden. Der Namensraum ist auch hier Std. IHnnter^rundlniol Du hast ja bereits gesehen, dass emige Behälter eigene Algorithmen wie sort () oder f ind() implementieren. In solch einem Fall solltest du natürlich den Algorithmus des Behälters bevorzugen, weil dieser immer etwas besser als der globale Algorithmus ist. Viele Algorithmen, die du verwenden kannst, benötigen als Argument häufig eine Funktion, um bspw. mit dem einzelnen Element zu arbeiten (ausgeben, vergleichen, ändern etc.). Hierbei kannst du bspw. eine gewöhnliche Funktion oder ein Funktionsobjekt (und seit 0*1 1 auch Lambda-Funktionen) verwenden. Ein Funkt ionsobjekt (du kennst es bereits als Funktor) ist nichts anderes als eine Klasse, bei der du den Operator ( ) überladen hast. CinfUhrun^ tu dl« Ui&r«ry <SH> 581
Ein einfacher Codeausschnitt dazu: void print(consc ElneKlassei obj) { *2 cout « obj.getValO « ’t’; > for_each(vec01 .begin(), vec.01 .end () , print >1 ’1 •i Der Aigonthmu$ for_each() ruft für jedes Element von vccOl .bcgin() bis vccOl ,cnd(> die Funktion print () auf. ‘2 Du rtt die Funktion print ( ) welche van f or__each für jede!, (fernen! jiufgerufpn wird. Statt mit einer Funktion kannst du dasselbe mit einem Funktionsobjekt wie folgt realisieren: "1 Hier ist nun das dritte Argument ein temporäres Objekt der KUisc EineKlassePrint. welches <n den pawnrfen Parameter kopiert und dessen Kopie dann für jedes Element verwendet wird. dass EineKlassePrint { *2 private; int ent; public: EineKlassePrint() { cnt»0; } -EineKlassePrint() {} void operator() (const EineKlassek ob j ) { *3 cout « ++cnc « cout.wldth(4); cout.setf(ios::left); cout « obj.getValO « 1 } >; •2 Definition des Funklions« Objektes' Kann natürlich auch als Template Implementiert werden, "3 Pur ein Objekt vom Typ EineKlassePr int kann jetzt der Operator ( ) mit einem E incK la S S C Objekt als Argument aufgeriden werden. for_each(vecO1.beginf), vecOl.end()p EineKlassePrint()); ’1 (Abhcd Der C++11-Standard bietet neben einer Funktion oder einem Funktionsobjekt auch eine Lambda-Funktion an. Eine Lambda- Funktion ist eine anonyme Funktion, die du genau an der Stelle implementieren kannst, wo du sie brauchst! STL selbst stellt natürlich auch einige solcher Funktionsobjekte zur Verfügung. Hierzu ein Überblick über die gängigen vordefi- nierten Funktionsobjekte. 582 SC(M?tKW
Arithmetik 1 plus<T> Xval+Yval tninus<T> Xval-Yval multiplies<T> Xval*Yval divides<T> Xval/Yval tDodules<T> XvalZYval negate<T> -Xval Arithmetische Funktion »Objekte equal_to<T> Xval==Yval not_e qual_t o<T> Xval!=Yval less<T> less_equal<T> Xval<Yval Xval<=Yval greater<T> greater_equal<T> Xval>Yval Xval>=Yval FunMiotfiSofejrktr für d-rn Vergleich I entspricht ! logical_and<T> logical_or<T> logical_not<T> Xval&&Yval Xval||Yval IXval logische FunArttonsobiekte •1 Hiermit werden die einzelnen Clemente von bcgin( ) bis cnd( ) in absteigende« Form igreater<int>) sortiert. (Zettel] Oie Funktionsobjekte werden auch Prädikat genannt. Hierbei gibt es einstelli- ge Prädikate (mit einem Argument, wie bspw. logicftl_not<T>) und zweistellige Prädikate (mit zwei Argumenten, wie bspw. less<T>). Um hierbei nun einen einfachen vector<int> mit dem Algorithmus sortO in absteigender Reihenfolge (ohne Angabe wird ja automatisch lcss<T> verwendet) zu sortieren, gehst du wie folgt vor: sort( vec.beginO, vec.endO» greater<int>() >; *1 le von Her Niehl immer müssen die Iteratoren funktionieren, wie eben auch gewöhnliche Zeiger funktionieren. Daher gibt es auch spezielle Iteratortypen. Denn was nützt es mir, wenn ich meine Daten ordentlich Im Behälter gepackt habe, aber nicht vernünftig darauf zugreifen kann? Keine Sorge, jeder Behälter bringt dabei seine eige- nen Iteratoren mit, und die sind intelligent genug, um schon das Richtige für den passenden Behälter zu tun: ” Input-lterator: Mil diesem iterator wird nur lesend auf die Objekte zugegriffen; er kann nur vorwärts bewege werden und ist daher für nur einen Durchlauf geeignet. Das bedeutet, für diesen Iterator muss nur der lesende Zugriff (*), Iterator weiter- selzen (++) und Iteratoren vergleichen (==; !=), definiert sein. Der bekannteste Ver- treter dürfte hier der istream-lteraior sein. CinfUhrun^ In <ti« lt«n«r<l r«.»pl«t» li&rery <$H> M3
* Output I terator Das Gegenstück zum Input Operator; es kann daher nur für den schreibenden Zugriff in der Vorwärtsbewegung verwendet werden. In der Praxis sollte immer nur ein Output-Operator auf einem Behälter verwendet werden. Vertreter hier- von sind bspw. derostream oder inserter-lteraior. Forward-Iterator: Der Iterator kann für den lesenden und schreibenden Zugriff auf ein Objekt in der Vorwärtsbewegung verwendet werden. Damit haben diese Iteratoren quasi alle Fähigkeiten von Input- und Output-Iteratoren. Allerdings ftlh mir kein STL-Behälter ein. der diesen Iterator verwendet. Ich denke, für eine einfache verkettete Liste würde er sich prima eignen. Somit macht STL derzeit keinen Gebrauch davon. Bidirektionaler Iterator: Entspricht in etwa dem Forward-Iterator, nur kann dieser zusätzlich noch rückwärts (--) bewegt werden, wie dies bspw. bei den doppelt ver ketteten Listen oder den assoziativen Behältern notwendig Ist. Random-Access-ltcrator: Der Iterator ist eine weiterentwickelte Form vom bidirektio- nalen Iterator, womit ein wahlfreier Zugriff möglich ist, wie dies bspw. beim Indcxopcrator [ ] und somit für vector und deque nbtig ist. Auch arithmeti- sche Operationen lassen sich mit ihm durchführen. 584 üpittl SC<MZ(MW
Jetzt noch zu einigen speziellen Adaptern, welche das Benehmen eines Iterators andern. Zunächst wäre das der Insert-Iterator, ein reiner Output'ltvrator, mit dein du etwas (wer hätte das gedacht) in einen Behälter einzigen kannst. Hierzu stehen dir mit front_inserter(Container) (vorne einfügen), back_inserter(Container) (hintenein fügen)und inscrter (Container, pos) (an pos einfügen) drei solcher Iteratoren zu Verfügung. Der Einsatz erfolgt natürlich auch zweckgemärt, so kannst du nicht einfach hergehen und front_inserter (Container) auf einen vector verwenden, weil es in einem Vektor rein logisch nicht möglich $ein soll, am Anfang etwas clnzufügcn. Zum Beispiel: vector<int> valdl; ‘1 fugst du den Inhalt von vecO 1 .ins Ende von vecO 11 Di» «»doppelst quasi vecOl Würdest du h er stattdessen front_ inscrterC) oder InserterO verwenden, würde sich das ftogranmi nicht übersetzen lassen, weil vector das nicht erlaubt. copy(vecOl.begLn(), vec01.end(), back_inserter(vec01)); *1 Für Ein- und Ausgabeströme gibt es die Stream-Iteratoren. Dabei gibt cs logischer- weise einen ostrearo_iterator und einen istream_iterator. Um diese Iteratoren zu verwenden, musst du den Header <f stream> einbinden, Mit diesen Iteratoren wird cs quasi möglich, dass du die STL-Algorithmen direkt auf Dateien oder auf der Eingabe (ein) und Ausgabe (cout) durchführen kannst. Zu guter Letzi gibt cs noch einen Reverse-Iterator (einen bidirektionalen Iterator), bei dem die Bedeutung von ++ und — vertauscht ist. Damit wird quasi ein Behälter mit ++ rückwärts durchlaufen. Der Beginn und das Ende eines solchen Behälters werden mit rbegin( ) (Letztes Element) und rend( ) (erstes Element) markiert. Fast alle Behälter von Dr. STL können diesen Reverse-Iterator verwenden. [gtHohnungl Du darfst jetzt aufwachen und dich in die Werkstatt begeben. Ich weiß, dass dieses Thema jetzt nicht prickelnd war. aber es lasst sich nicht vermeiden, einen Übergang zwischen den Behältern und den Algorithmen herzustellen, um bei der Verwendung von Algorithmen auch das Warum zu verstehen!!’ Cirwf^hrun^ In tfi« St+ndird TupUti Lfcbr«ry (SH) MS
Die Hilfsmittel für Fertigkuchen und Zutaten Im Einsatz Bezogen auf das Büro, wird es jetzt Zelt für etwas Praxis, Zunächst möchte ich dir hierzu natürlich ein Listing demonstrieren, welches ein Funktionsobjekt verwendet. Anhand des Listings darfst du danach eine Aufgabe lösen. dass EineKlasse { *1 int irgendwas; public: EineKlasse! int i 1 ) : irgendwas(1) {} -EineKlasse!) 0 int getVal() const { return irgendwas; } void setValünt v) { irgendwas - v; } friend bool operator*! const EineKlasse^ vall const EineKlasseS va!2 ) ( if( vall.irgendwas > va12.irgendwas ) { return true; > eise { return false; 1 >; *1 dass EineKlassePrint { *2 private: int ent; public: EineKlassePrint() { cnt“0; } ’2 Da h<r de« operator!) definic*t wurde, kannst du gleich ertennen. dass es sich hierbei um ein Funktions- Objekt handelt. Das Objekt kümmert sich um die saubere Ausgabe eines Elementes vom Typ EineKlasse -EineKlassePrint!) O void operator!) (const EineKlasse6c obj) cout « ++cnc « **:”♦ cout.width(4); cout.setf!ios::left); cout « obj.getVal!) « ‘ ‘; } ’2 >; vector<EineKlasse> vecOl; *3 int val; { *2 3 Dem vector IbßCfi wir । n einer Schleife neue Wrrtr am Ende h.nzu, ta der Ap-wendcv den Wert 0 ein^egeben hat. 586 SCCNZtMk
for(;;> { cout « "int-Wert eingeben: if( (Kein » val)) | | val <» 0 ) break; vecOl.push_back( EineKlasse(val)); ‘3 } for_each(vecOl .begln(), vecQl.endO, EineKlassePrint()); Cout « endl; sort( vecOl.begin(), vecOl.end(), greater<EineKlasse>()) for_each(vecOl.begln(), vecOl.end(), EineKlassePrint()) cout « endl; ‘4 Mit for_each() ruht du jetzt für jcdc5 Element vom Bereich vecOl .begin() bis vecOl .end () das Funktionsobje*1* EincKlasscPrint() auf rithmus sort ( ) h ngrgen sortiert h>?r die Elemente tcQL.beginQ b.s vecOl. end( ) und verwendet Irhniertc Objekt greater<>( ) .iB Funktiansobjekt rhert wird »mit in absteigender Reihenfolge EineKlasse such klappt, haben *irden >-Operator in der Klasse definiert. lEiftfiche Aufgabe] Erstelle für das Listing jetzt em weiteres Funktionsobjekt,, mit dem nur die ungeraden Zahlen ausgegeben werden. for_each(vec01.begln(),vecOl. end(),EineKlasseUngerade()); *1 L *1 Deine Aufgabe soll es sein, Vm Funktion^obfCkt mit dem Nam EitaKlasseUngeradezue nur ungerade Zahle Bohrtelinm au dass EineKlassellngerade { public: void operator() (const EineKla$$e& obj) { *1 if(obj .getValO 22) { *2 cout « obj.gctValQ « > > >; Gar nicht schlecht. Schrödinger! 5B7 (jnführun9 in die ’2 Gibt es hiei einen Rest mrüik ’1 Unser Merkmal fOr ein Funktionsobp'kt ist hier der definierte ( ) -Operator. ßeben diesen *3 ... haben wir
Hilfe für den Iterator Djmit du dich später nicht mit Kleinigkeiten bei den Iteratoren abquälen musst, gibt cs auch noch ein paar nützliche Hilfsfunktionen gratis obendrein. Damit kannst du dir somit das Leben erheblich leichter machen: void printfconsc inc& obj) { cout « obj « ’,•; vector<int> vecOl(10); for(sizc_t i’O; i < vecOl.sizc{); !♦♦) { vecOl.at(i) - i; > vector<inc>::Iterator it • f ind( vecOl. beglnO, vecOl.end(), 5); *1 if( it == vecOl.end() } { return -1; > cout « distance(vecOl.begin(), it) « endl; *2 advance(it, 2); '3 cout « *it « endl; iter_swap(it, vecOl.beginO); *4 for_each( vecOl .beginQ, vecOl. end(), print}; cout « endl; 1 Such nach einens Element mit dem Wert 5! ‘2 Hierbei wird mit distance(it1, it2)de» Abstand zwischen dem Internier des ersten und dem des zweiten Argumen- tes zuruckgegeben. "4 und nut der Hilfsfunktion itcr_ swap ( it 1, it2) vertauschst du ganz einfach die beiden Elemente, auf die diese Iteratoren verweisen. 3Mit advance(it, pos) veschiebst du den Iterator des ersten Argumentes um de Anzahl der Positio- nen, welche du ats zweites Argument angibst 588 c«e-.t«l SCCHZCwh
Allmählich wird es öde... Komm schon. Schrödinger! Ich weiß, du willst unbedingt mit den Algorithmen weiter- machen. und das Thema war jetzt nicht gerade sehr aufregend. Aber es ist unbedingt nötigt, dass du die Funktionsobjekte und Iteratoren etwas besser kennenlernsc. Sonst mussten wir bei den ?\lgorilhmen diese Themen zusätzlich mir reinquetschen. Stattdessen können wir uns dann gemütlich zurUcklehnen und uns ein wenig die vorhandenen Algorithmen ansehen. CinfUhruo^ L» all T»»pl«Ct llferiry <STl> MC
Eine sehr gute Frage, Schrödinger! Im Grunde ist es aber auch nicht viel schwerer als ein einstelliges Prädikat. Ich will es anhand eines einfachen Beispiels schnell zeigen: bool myequal(int objl, int obj2) { *1 return (obj i”obj2); > *1 Hier iit airücfot elrw einfache tunkVon. welch? tm-i int-Wrrk- mrtelruinrfcr vector<lnt> vecOl(10); voctot<int> vcc02(10); '2 Und hier cm Algorithmus, der das zweistellige Prädikat verwendet Der Algorithmus equO 1 ( ) vergleicht den Bereich vor vccOl .bcgin() bis vecOl .bcgin() for(size t 1=0; i < vecOl.size(); i++) {| 'mit dem Inhalt ab vec02. bcgin( ) bis zum Ende mit dem vecOl 'tt(i) • i*l" zweistelligen Pridikat myequal. Wenn alle Vergleiche von myequa 1 ( ) immer true zurückgeben, und auch beide vecQ2.at(i) 1+1; BeUlterb*szum gewünschten Bereich identisch. > bool b equal( vecOl.begin(), vecOl.end()t vec02.beginf), myequal ); ‘2 if( b true ) { cout « "Beide Vektoren sind identisch!\n"; > Ich habe dir hier die Funktionsvariantc des zweistelligen Prädikates demonstriert. Aber auch mit dem Funktionsobjekt sollte das kein Problem für dich sein. Elier das passende Funkcionsobjekt dazu: dass Myequal { public: bool operator()(int objl» int obj2) { return (objl"»obj2); > IHotlll (Zett«n An dieser Stelle muss ich natürlich hinzufügen, dass in diesem Beispiel ein extra Prädikat total überflüssig ist und auf das vierte Argument von auch ganz verzichtet werden kann, weil Intern in diesem Fall das vordefinierte Prädikat verwendet wird. equal_to<int>() Ich weiß nicht, ob ich es schon erwähnt habe, aber zur Sicher- heit solltest du dir hier noch notieren, dass du den Operator () natürlich im public-Bert ich schreiben musst! 590 C«»kt*l SCCHZCHH
Der fehlerhafte Code: for_each(vec01.rbegin(), vecO1.rend(), print>; cout « endl; Itiafochr Aufgabe] Folgender Codeausschnitt löst eine Speicherzugriffsverletzung aus! Hier wurde versucht, einen Behälter von hinten nach vorne auszugeben, Was wurde hier falsch gemacht, und was kannst du machen, damit das trotzdem funktioniert? for_each(vecOl.end(), vecOl.begint), print>; *1 cout « endl; *t Hiermit wird ein .Segmentation fault' dusgdöst. Natürlich. aber sei gewarnt, dass noch viele C*+-Compiler hinter dem neuen C++11 -Standard hinterherhin- ken und es bei dir vielleicht noch nicht funktioniert. Guck dir folgende Codezeile mit einer Lambda-Funktion an: for_each(vecO Kbeglnf), vecOl*end(), (l(lntval) { cout « val « endl;} ); "1 cout « endl; 4 Das ( | bedeutet keine 8indung’ Mit [] werden die Werte kopiert und mit (&] eben referenziert. Aber dies nur am Rande. "1 Die Umtxfd-Funktion fangt mit dem ( ] zu* Bindung an die Variablen des IdkoJun Bet eit hei an. Zwiwhen den Klammern ( ) stehen die Argumente (hiet nur val), und{ } iit der Funktion^' kor per. Ziemlich einfach, hm? als kleine Notiz. ItFlataungl Perfekt!!! Jetzt ist es wieder Zeit für eine Entspannung. Deine Freundin hat dir eine Lasagne gemacht! Danach wirst du dich wohl wieder um dein WoW-Leben kümmern müssen!!! CinfQhrun^ In al« St*nd«rO li&r«ry <$H> 591
Nachdem du deine Zutaten in den Behälter gepackt hast, wird es Zeit, dass du damit auch was machen kannst. Hierzu stehen dir viele Algorithmen zur Verfügung. Du weißt ja mittlerweile, dass du einem Algorithmus keinen ganzen Behälter über geben musst, sondern nur bestimmte Bereiche, die du mithilfe von Iteratoren abgibst. Des Weiteren gibt cs für fast alle Algorithmen eine Version mit und ohne Prädikat (Funktion, Funktionsobjekt oder Lambda Funktion). Der Fundus der Algorithmen ist enorm, weshalb du hier logischcrwcisc nur einen Rundumschlag dazu bekommst. [Hin1«rgrundin1t>] Für mehr Details wie Syntax und Anwendungsbeispiele dieser Algorithmen empfehle ich dir, unbedingt die Dokumentation deines Compilers zu verwenden oder meine Lieblingswebseite http://www.cpluspiui.com/reference/aigodthm/ dafür zu besuchen. Damit wird nur lesend auf die Behälter zugegriffen. und es wird nichts verändert. Diese Algorithmen lassen sich auf alle Anon von Behälter an wenden. lZ't"l) Suchfunktionen geben immer die Position des gefundenen Elementes zurück oder, wenn nichts gefunden wird, das Ende des gesuchten Bereiches. I Algorithmus 1 Was ich damit machen kann ... for_each(il, 12, func) Führt für jede-s einzelne Element aus dem Bereich 11 bis i2 die Funktion funC auf (kann auch ein Funkionsobjekl oder eine Umbda-Funkbon sein). findfil, 12, val) Sucht Im Bereich i l bis 12 nach einem Element mit dem Wert val. find_if(il, 12, pred) Sucht im Bereich 11 bis 12 nach einem Element, welches de« Bedingung pred entspricht. find_end(H,12, jl, j2) Sucht im Bereich 11 bis 12 nach einer letzten find_end(il,12,J1,j2, passenden Teilfolge vom Bereich j 1 bis j 2 oder pred) danach, dass eben das Prädikat pred wahr ist. 692 scchzchn
Aleorithinus Was ich damit machen kann find_first_of(11,i2, find_first_of(11,12, jl,j2,pred) adjacent_find(ilt12) adj ac ent_f ind(i 1Y i 2, pred) Entspricht f ind—end(), nur dass nach dem ersten Auftreten mehrerer Elemente gesucht wtrd, Hiermit suchst du nach tefMChbarteo Elementen zwischen i 1 und 12. deren Werte gleich sind bzw. wodai Prädikat prcd true zurückliefert count(11>12 rval) count_if(il, 12, prcd) mismatchdl, 12, j 1) mismatch(il,12,j1,pred) Gib! die Anca, hl der Elemente au> dem Bereich 11 bis 12 itiritck, die den Wert val haben «der wo das Prädikat pred true ruruckgibt. Liefert die erste Position zurück, an der der Bereich 11 bis 12 ab j 1 bis tum Ende nicht ubereinstimmt oder das Prädikat pred true ruriickgibt. Zurückgegeben wird die Template- Klasse palt, womit du die erste Nichtüberein- stimmung von 11 bis 12 in first und von j 1 in second findest. equal(ll, 12,j 1) equ<il(il, 12, j 1 ,prcd) search(11,12,j1,j2) searchdl,12,j1,j2,pred) search_n(il,12,n,val) scarch_n(il,i2,n,val,pred) min_eletnent( 11,12) niax_element( 11,12 ) Algorithmen. welche nix am ÖeMIte* verändern Vergleicht die il bis 12 ab dem Bereich j 1 auf Gleichheit, oder das Prädikat prcd liefert true zurück. wenn beide Bereiche gleich sind. Sucht im Berttch 11 bis 12 nach der Teilfdge J 1 bii j 2 auf eine Übereinstimmung oder wo das Prädikat pred true zurückgibt. Sucht im Bereich il bis i2 mit der längt- n nach einer Tedfolge mrt dem Wert val oder wo das Pr.idikat pred true zurückgibt. Liefert das kleinste bzw. größte Element aus dem Bereich il bis 12 zurück, Modifizierende Algorithmen Wie du am Namen vielleicht schon erahnen kannst, greifen diese Algorithmen direkt auf den Inhalt der Behäl- ter zu. Du kannst damit quasi die Elemente selbst ändern. Ist der Zielbereich ein assoziativer Behälter, dann kann der Algorithmus d^pilt nicht verwendet werden. CinfU {Achtung] Wenn du etwas in einen extra Ziel- bereich schieben willst, musst du schon selbst darauf achten, dass dieser groß genug Istlll 503
| Algorithmus 1 Was ich damit machen kann ... copy(il,12,ol) copy_backward(il,i2,ol) Kopiert die Elemente vom gereich 11 bis 12 in den Zielbercich. der mit ol beginnt. copy_backward ( ) macht dasselbe, nur rückwärts, und hier endet der Ziel bereich mit ol swap_rangcs(il,12,ol) Damit tauschst du die Elemente im Bereich 11 bis 12 mit den Elementen, die bei ol beginnen. iter_swap(il, ol) Tauscht den Inhalt, auf den 11 verweist, mit dem Inhalt aus öl transform(il,12,01,pred) Damit rufst du für jedes Element im Bereich 11 bis 12 die u na re Funktkm pred auf und speicherst den Rückgahewcrl im Zielbereich ol Quell* und Zielberekh dürfen gleich sein! transformtil,12,j2,ol,pred) Ruft für jedes Element w>m Bereich 11 bis 12 und jedes Element ab j 2 die binare Funktion pred auf und weist o l den Ruckgabewert zu. replaccfll,12,oval,nval) replace_if(il,£2,pred,nval) Damit ersetzt du in den Bereichen 11 bis 12 die Elemente mit dem Wert oval brw. dort, wo das Prädikat pred true zuruckgibl, mit dem Wert nval. rcplacc_copy(i1,i2,o2, oval,nval) replace_copy_if(il,i2,o2, pred,nval) Entspricht rcplaccO bzw. rcplacc_ 1 f ( >. mit dem Unterschied, dass zusätzlich noch in den Zielbereich o2 kapiert wird, wobei sich hier Quell- und Zitlberekh nicht überlappen dürfen. fllltil, 12, val) fill_n(il, n, val) Füllt alle Elemente im Bereich 11 bis 12 mit dem Wert val. Im Fall von f ill_n( ) werden ab 11 insgesamt n Elemente mit dem Wert Vfll gefüllt. generate(il,i2,func) gencratc_n(il,n,func) Ensprrcht f 111( ) brw. f lll_n( J, nur wird statt eines Wertes der Bereich i 1 b«s i2 bzw. R Elemente ab 11 mit dem Rückgabcwrrt der Funktion fimc gefüllt. Modi’fiiitrende Algorithmen von STL Mit diesen Algorithmen werden Elemente aus einem Bereich entfernt. Allerdings werden die Behälter dadurch nicht .kleiner*, sondern cs werden nur gelöschte Elemente überschrieben. Im Grunde handelt cs sich hierbei Ja auch um modi fixierbare Algorithmen, und dir sind auch nicht auf assoziative Behälter anwendbar. 584 SCCM2CMH
Algorithmus |Hint»rgrundinto| Willst du die Anzahl der überschüssigen Elemente im Behälter nach einem löschenden Algorithmus entfernen, so kannst du dies mit der Elementfunktion erasef} des ent- sprechenden Behälters machen. | Was ich damit machen kann ... remove(11,12,va1) rcmovc_if(i1,i 2,pred) rcmovc_copy{il,12,ol,val) rcmove_copy_i 1(11,12, o2,pred) unlquefil, 12} unique_copy{11,12 ,ol) Loscht allr Elemente im Bereich 11 bis 12. die den Wert Vfll haben oder wo das Pradikai pred true turuckgibl. Entspricht rcplacc_copy(> und rcplacc_copy_if (). nur dass die rCttOVC-Vertior» zum Loschen statt rum Ersetren benutzt wird. Loscht alte Elemente, bei denen der Vorgänger denselben Wert besitzt. unique_copy( ) hingegen kopiert die Elemente noch in den Bereich o2 Afgonthmen zum loschen von Elementen (Zettet] An dieser Stelle sollte natürlich noch darauf hingewiesen werden, dass die gelöschten Element nach hinten wandern und die remove-Funktionen einen Iterator auf das erste gelöschte Element zurückgeben. Hierbei gilt auch, dass alles zwischen dem zurückgegeben Iterator und cnd() ungültig ist und schließlich entfernt (bspw. mit eraseO bei vector) oder ignoriert werden kann. Ein mutierender Algorithmus ändert nur die Reihenfolge der Elemente Im Behälter und lässt sich daher auch nicht auf assoziative Behälter anwenden. i Was ich damit machen kann rotatef11,12,13) rotate_copy(il,12,13,ol) Damit führst du eine Rotation im Gereich i 1 und 13 durch. und 12 rst anschließend das erste Element. Mit VOtate_COpy ( ) wird da-s Ergebnis in ol kopiert. random_shuffle(11,12) random_shufflc(ll,12, func) Ändert die Reihenfolge der Elemente im Bereich 11 bis 12 zufällig oder anhand der Funktion f unc. rcvcrsc(il,12) reversc_copy(11,12,02) Damit drehst du die Reihenfolge vom Bereich 11 bis 12 um. reverse_COpy ( ) macht das auch, .Iber kopiert diesen Bereich nach o2 partition(il,12,pred) stable_partition(11,12,pred) Damit zerlegst du den Bereich il bis 12 anhand von pred in zwei Bereiche: einen Bereich, der true zurwckliefert, und einen zweiten Bereich, der all Ruck* gabewert zurückgegeben wird, worauf du mit einem Iterator zugreifen kannst Bel Stablc_partation bleibt die relative Reihenfolge erhalten, ncxt_pcrmutatlon(ll,12) prev_pertnutocion( 11,12) Damit kannst du die nächste oder vorhergehende Permutation (Kombination) der Elemente im Bereich 11 bis 12 erzeugen. MuCitrmde Algorithmen Cfcnfü-hrung Lr> tfia SUndirj U&r<ry (SH) 618
Beim Sortieren wird die Reihenfolge der Elemente geändert, und daher Ist es auch nicht anwendbar auf assoziative Behälter. Sortiert wird entweder nach dem Standardkriterium (les S<>) oder einem Prädikat, wie einer benutzerdefinierten Funktion» einem Funktionsobjckt (Funktor) oder eben einer Lambda-Funktion. 1 Alrorithmus Was ich damit machen kann ... 3ort(il,12) sort(il,12,pred) stablc_sort(il,12) stable_sort(il,12,pred) Damit sortierst du den Bereich 11 bis 12 standardmäßig auf- steigend (less<>) oder gemäß dem Prädikat pred. Bei StablC—SOrt ( } bleibt d»e Reihenfolge der Elemente erhalten. partlal_sort(ii,12,13) partial_sort(11,i2,13,pred) Sortiert die Elemente aufsteigend Bereich 11 bi* 13. bis de Be- reiche 11 bis 12 sortiert sind, oder anhand des Prädikates pred partial_sort_copy(11,12 ol,o2) partial_sort_copy(11,12, ol,o2,pred) Sortiert die Elemente im Bereich i 1 bis 12 und speichert das Ergebnis im Bereich ol bis o2 Ist der Bereich ol bis o2 kleiner Als 11 bis 12. wird nur entsprechend partiell sortiert, was eben in Ql bis 02 passt- Alternativ gibt es auch noch eine Version mit dem Prädikat. nt_element(il,inth,12) nt_clemcnt(11,inth,12,pred) Sortiert die Elemente so. dass inth tatsächlich an der Position ist, wo es bei einem normalen Bört () stehen würde. Im Bereich 11 bis inth hingegen sind nur Elemente, die kleiner smd, und zwischen inth und 12 dir Elemente» die großer als inth sind. Mit dem Prädikat pred kannst du die Sortierreihenlolge selbst festlegen. makc_hcap(i1,12) make_heap(il,12,pred) Macht aus dem Bereich il bis 12 einen Heapl Ein Heap ist eine spezielle Datenstruktur, die als Baum implementiert ist. pop_heap(il,12) pop_heap(11,12,pred) push_hcap(il,12) push heapdl ,12,pred) Mit pop_heap entfernst du das erste Element im Heap und fügst es als letztes Clement wieder ein. Mit push_heap ist es genau umgekehrt. sort_heap(11,12) Sortiert die Elemente i 1 bi* 12 des Hcaps und verwendet dabei den sehr effektiven Hrap-SorbAlgarithmus. Anschließend ist dieser Bereich kein Heap mehrt Algorithmen zum Sortieren iH'nterg rundinfol Bei einem Heap wird, vereinfacht ausgedrückt, aus einem sequenziellen Behälter ein binärer Baum, Natürlich nicht richtig. Aber das Verhalten des Behälters ahmt einen Baum nach. Daher ist es anschließend beim sequenziellen Behälter, der ein Baum sein wollte, auch so, dass das erste Heap-Element das größte ist. Das Prinzip ist dann das gleiche wie bei einer Prioritätsschlange. wie du dies von priority_queue her kennst. 596 c«0;t«l SCCHftMh
Ist der Behälter bereits sortiert, so kannst du einen der folgenden Algorithmen verwenden. Danach bleibt natürlich alles nach wie vor sortiert. Aljflnithmrn für b*reH> twich? Hwähnt wurden hier die numerischen Algorithmen, Ife du diverse Berechnungen auf den Behältern kannst. Dabei handelt es sich um die Algorithmen accumulacc().inncr_product(), partial_sum() und adjacent^different(). welche alle im Header <numeric> enthalten sind. Ci»ruhru«n in dl« St«nd«rd T««pl«t« li&r«ry <$TL> 597
Algorithmen verwenden </&$. /)r^ S>t £- da 3ft> lEimfiche Aufgabe] in der Tai Ist die Anzahl der Algorithmen sehr beeindruckend! Und nebenbei erwähnt, bin ich jetzt echt froh, dass wir zur Pra xis übergehen können. Die Theorie in den Tabellen war selbst für mich ziemlich langatmig zu erstellen. Mit dem folgenden kleinen Codeausschnitt habe ich mithilfe des Algorithmus generateO den Vektor vecOl mit fünf (Pseudo-)Zufallszahlen gefüllt. Deine Auf- gabe soll jetzt sein, jeden dieser Werte zu verdoppeln und die Ergebnisse in einem weiteren Vektor vec02 zu sichern Dann solltest du noch beide Behälter vecOl und vec02 sortieren und zu einem Behälter vec03 zusammenmischen. Achte außerdem darauf, dass keine doppelten Elemente in vecOl enthalten sind. Gib vec03 am Ende auf dem Bildschirm aus. Ob du für die Prädikate nun eine Funktion, ein Funktions- objekt oder eine Lambda-Funktion verwendest, überlasse ich dir, auch was für Algo- rithmen du hierbei auswahlst! Es gibt immer mehrere Wege, die ans Ziel führen. |Afhlun{| Bedenke, dass nicht alle Compiler den C++11-Standard voll unterstützen und du somit womöglich nicht die Lambda* Funktionen verwenden kannst. In dem Fall musst du demen Code eben entsprechend anpassen und eine Funktion oder ein Funktionsobjekt stattdessen verwenden, Allerdings solltest du damit keine Probleme mehr haben. Der Code, mit dem die Zahlen erzeugt werden: linclude <cstdlib> // für rand() *1 Hier werden die (Pseudo-) “* ZuWkzahlen gespeichert. vecO 1 (5); *1 vector<int> vec02(5); 2 Hier sollst du «bewerte von VCC 01 wrdoppe* speichern. 598 SCCM2CMH
vcctor<int> vcc03(10); "3 Und li er IAndern dm Code die Werte von vecOl und vec02 w>i1»ert und ohne einen doppelten Wert generate(vec01.begin().vecOl.end(). [HXretum (rand()I100);} ); M ’4 Mi1 generate() rufen wir für jedes Element vom Anfang bis zum Ende von vecO 1 die Lambda Punktion aut welche hier mit rand ( ) eme (Pseudo-)ZufalSszahi zufückgibt. •1 Mit Ctansförmi ) führe xh für alle Elemente Im Behälter vecOl d*e Lambda- Funktion (letztes Argument) aus und spexhe/e den Rückgabe-wert in vec02. *2 Hier UHtiere ich beide Behälter aufsteigend. cransform(vec01.beglnt).vecOl>end(). vecQ2-begln(), [)(int val)(return val*2;} ); so«(vecOl *begin(), vecOl *end()); *2 sort(vec02.begin()wvec02.end()); *2 uniquc(vcc01 .bcgin(), vccOUcndf)); *3 unique(vec02.begin(), vec02*end()); *3 merge( vecOl.begin().vecOl,end(). vec02>begin(), vec02*end(), vec03 *1 *3 Jetzt entferne ich alle doppelten Elemente aus den einzelnen Behältern. for_cach( vec03.beginf)> vec03.end()» [)(int val) (cout « val « M, *;} ); *5 cout « endl; •4 Mit tficrge ( ) m«$ch»e ich d«e beiden sortierten Behälter zusammen und schreibe den kompletten Inhalt sortiert in vec03 ’S Und am Ende gebe ich den kompletten Inhalt von vec03 auf dem Bildschirm aus. Auch hier verwende ich wieder eine neue moderne Lambda-Funktion dafür. (Code bearbeiten) Fast perfekt, aber da ist trotzdem ein ganz kleiner Schönheits- fehler drin. Du führst zwar uniquef) auf beide Behälter aus, damit keine doppelten benachbarten Elemente mehr vorhanden sind, aber es ist ja nicht ganz ausgeschlossen, dass es nach mergel) doch wieder gleiche Elemente gibt. In unserem Fall zwar schon, aber in der Praxis werden die Behälter eher selten mit Zufallswerten gefüllt. Hier würde ich dir empfehlen, ent- weder uniqucl) nach mergel) auszuführen oder statt mergel) den Algorithmus eet_union() zu verwenden. CinfQhrun^ in di« ti«nd«rd li&r«ry <$Tl> MB
Das wäre doch eine prima Aufgabe für uns?! Allerdings kann ich dir jetzt gleich schon sagen, dass deine Freundin hier besser noch weitere Zahlen wissen sollte, weil es doch wohl zu viele Kombinationen gibt! t Naja, besser als nichts, aber deine Freundin wird heute noch ziemlich lange damit beschäftigt sein, das Passwort einzugeben. Soviel kann ich dir versichern. Wir brauchen also eine Permuta- tion! Dafür haben wir ja auch zwei Algorithmen. So würde ich das machen: "1 Hier ist zunächst unser Vektor, den wir mir den Zähren 1 bis 9 tonen. vectordnt* passwort(9); '1 for(size_t i»l; i<=passwort.slze(); ++i) { Passwort,at(i-1)!; *1 unsigned int counc • 0; '2 *2 Damit zahlen wir die Anzahl der möglichen Kombinationen für 1*’*’*74. *3 In der Schleife durchlaufen wir jetzt alle möglichen Kombinationen, die sich aus dem Vektor mit den Zahlen 1 bis 9 ergeben können. while(next_pernutation(passwort.beginf),passwort.end())){’3 if( (passwort[0] 1) ü (passwort[7] 7) 400 SC<HZCHH
‘4 Wir sind nur an den Kombi n.ibonen aus 1”’”74 interessiert ... 44 (Passwort[8] " 4) ) ( *4 for_each( passwort.begin()* passwort.end()„ ()(int val) (cout « val;} ); *5 COUt << endl; *5 . und flehen di<te auf dem ßikhthirm jiu; (wir COunt++; *6 /ä^^Jverwciidcn hier wieder C»+11-UmM* für dw Pradlkal; wenn es nicht geht, mach eine Funktion oder e<n Funktionsobjekt daraus). cout « count « " Kombinationen\nw; B6 "6 Und natürlich rAhlcn wir die Anzahl dieser Kombinationen mit. Wünsch deiner Freundin viel Spaß beim Tippen Permutation Aal u«»er Problem pjiisco 7JO Kornbimläonrn. 198526374 198532674 198S36274 198562374 198563274 % 198623574 198625374 198632574 198635274 b 198652374 > 198653274 - 720 Konbinatianen Schroedinger S ClnfUhrung In dl« Standard Library CSTL> 601
Ende gut, alles gut In der Werkstatt hast du jetzt gesehen, wie elegant sich Algorithmen ganz einfach in deinem Programm einbauen lassen. Die einzige Schwierigkeit dürfte es wohl sein, den Überblick zu behalten und zu wissen, wann man welchen Algorithmus am besten verwenden kann. Aber je öfter du sic in der Praxis einsetzt, desto leichter wird dir das fallen. Du konntest im Abschnitt zuvor sehr schön sehen, wie du dank der Algorithmen mit ein paar Zeilen Code ein Problem lösen kannst. JEiatAthe Aulgabr] Okay! Guck dir folgenden Code an und beschreib mir bitte anhand der entsprechenden Nummern, was hier genau gemacht wird! Verdeck am besten die Lösung! <02 scenftHH
Das Codebeispiel: bool geradefint val) {return(valZ2); ) list<int> listeOl(lO); int carray[10]; generate( ListeOl.begln()♦ ÜsteOl,cnd(), ()(){retum (randOHOO)|} >| list<lnt>::iterator part, it; part = partltion(listeOl.begin(),listeöl•end( for(lt-liste01.bcgin(); itl-part; it**) { *2 cout « *it « ", 11; *2 > cout « endl; for(it=part; it!»listeOl.end(); it**) { "3 cout « *it « ", "3 cout « endl; iqrj ujpifjjS jap puj juauj^j3 ajija irpjni* ]iw<m ’jrq uaqaStolfcprijrv JNnLU upw () apejoS l*oasaqajw jnr 13-« mrd uaiM^Z aip wi pun uap^jaäun ?ip ipi$ uapoijaq iPi uji jnc uatqrz ^Ptj^ pufl 4pl*;röuiai ui ( )0pV403 jdp 3j| qpiU S<H> l|PQUl uop >|M UJ||JJ KMM ‘WH h »gerade); ’1 J- jnr uaiMVZ uapnaSijn .»p uaqaS iim Z, 'uJfqez uapejrf J’p ;aiq pun partial_sort_copy( HsteOUbeginO , ListeOl<end(), carray» carray*10 ); ’4 fort 6ize_t i-0; i<10; i*+) { cout « carrayti] « ", *; tfim»qipn««»JWi!8»wp>n» nqriknv jpiMg«!|q>suv »ip wm ‘tun Ab J JBO * J Xwjvowp «! (Qoasyx •*" ’iff COut « endl; tiMu->w|doj|punuajaiucmiuu^H ►. Prima! Sehr gut beschrieben. £ac^\i. £^i dass EineKlasse ( Int Irgendwas; public: H Alles wie gehabt !!1 Okay, das dürfte jetzt kein Problem sein, guck dir folgen* den Code dazu an: EineKlasse init(){ return EineKlasse((rand(>XLOQ)); } tinfUhru»? tn al« tt*nd,rO T»«pl«t, li^riry <Vl> M3
“1 Hier machen wir aus einem Vektor mit EineKlasse einen Keap. Grundvoraussetzung, damit dies auch in diesem F.i mit EineKlasse klappt, ixt, dass auch der < Operator implementiert Ht. sonst geht gar nichts1 *2 Wir durchlaufen unseren VeMor wie bei eine* priörity__queue, bis der Behälter leer Ist. *3 Da wir ja einen Heap haben, können wir siche* sein, dass immer das größte EFement ganz vorne am BehJJter ist "4 Okay, wir sind fertig damit und wollen das nächste größere Element aus dem Behälter haben. Nun plattieren w.ir somit das zuletzt größere Element ganz hinten ... '5 . wo wir es dann ganz einfach vector<EineKlasse> vecOl(10)t vec02(10); generatefvecOl*begln(), vecOl *end(), init); vecOZ 15 vecOl; make_heap(vecOl.be$in(), vecOl*end()); *1 while( IvecOl .emptyO ) { *2 cout « vecOl*front0»getVal() « endl; *3 pop_heap(vecOl.begin()t vecOl.end()); "4 vecOl,pop_back(); *5 > makc_hcap(vccO2.bcgin()» vccO2rend()); "6 sort_heap(vec02.begin()f vec02.end()); "7 for_each(vec02.begin(). vec02>end()> (](EineKlasse& val) (cout « val.getValf) « cout « endl; mit pop_back() hinten entfernen und wieder hoch zum $chlci1emnf.ing springen *6 Auch aus vec02 machen w r jetzt einen Meap . *7 , und sortieren diesen mittels des Hcap-Sort-Algorithmus Dieser Algorithmus ist in einer umfangreichen Sammlung von Daten sehr effizient Danach hegt allerdings kein Heap mehr vor’ Ein« muss ich hier noch kurz loswerden. Wenn dem Compiler sich bei folgender Zeile beschwen ... for_each(vec02.begin(), vec02.end(), (](EineKlasse^ val) (cout « val.getVal() « ", ";)); Mit folgender Fehlermeldung .. djnn brdrutpt nichts jrdr<»%. jlt drtn Compiler keine 1 .imbdj-Funirlaonrn kann u*d entweder nicht oder iwr teilweise dem O*11StaAd(ard) entspricht! If IBrlphfluHg] So, jetzt bist du durch mit der STL und kannst dich voll und ganz deinem Schnitzel mit Pommes widmen, das dir deine Freundin dank deiner guten Programmierarbeit gebraten hat? <04 Udtil SC<H?tKK
___SIEBZEHN- Schöne neue Welt C++11... Schrödinger kennt sich jetzt in sehr vielen Ecken des C++-Standards aus. Aber die Welt steht nicht still und Ist ständig Im Umbruch, so auch der C++-Standard! Daher wollen wir Schrödinger In diesem Abschnitt den neuen C++11-Standard etwas schmackhafter machen. Und da gibt es wirklich sagenhaft viele Neuerungen. Die Interessantesten habe Ich für Ihn herausgepickt.
•• 7«l lHintrr£rundinffl| Da sich auf dem allen C++98 (von 1998) allmählich die Spinn- weben breiimachen und neue, moderne Programmiersprachen (wie bspw, Java und .NF.D immer mehr aufholen, ist es Zeit geworden, dass auch C++ eine Auffrischung erhält. Zwar hat es 2003 (mit C++03) bereis ein kleines Facelifting bekommen, aber das war im Grunde nur Pipifax. Mitte 2011 allerdings war es endlich soweit (ich habe schon nicht mehr daran geglaubt), als die zuständige ISO-Behörde die bisher immer nur als C++0X bezeichnete Zukunftsmusik offiziell als C++ll-Standard über- nommen hat. Anhand des alten Codenamens C++0X kannst du schlussfolgern, dass das Teil schon vor 2010 hätte kommen sollen. Aber das Warten hat sich gelohnt, und aus dem neuen C++11- Standard ist mehr als wieder nur ein Facelifting geworden! Da sind wirklich ein paar feine Schmankerl dabei. Bevor ich dir hier allerdings ein paar meiner Favoriten aufliste, muss ich natürlich noch das leidige Thema loswerden, ob und wo du den neuen Standard überhaupt (zumindest teilweise) benutzen kannst. Glücklicherweise sichen dir gerade bei den bekannteren Compilern wie GCC und Microsoft Visual C++ 10 (oder neuere Version 11) die wichtigsten Neuerungen bereits zur Verfügung. Nicht vergessen sollte man auch den clang-Compiler, der künftig auf Systemen von Apple statt des GCC verwendet werden soll. Beim GCC musst du im Augenblick die Neuerungen noch mit dem Argument -std»c++0x (bzw -std=c++l 1 ab gcc-4.7.0) aktivieren, aber bei VC++ geht es schon von selbst! Eine gute Übersicht zu gängigen Compilern und deren aktueller Unter- stützung des neuen C++11 -Standards findest du hier: hl tp.7/wiki. apache. org/ftdcxx/%20C++0xCompHerSuppoft Aber genau dieses vermaledeite .Geht es jetzt bei mir oder nicht" hat mich dazu ver- anlasst, dir das Thema in einem extra Abschnitt zu erläutern, bis sich der neue C++11- Standard flächendeckend verbreitet hat. Okay, dann will ich dir mal auf den folgenden Seiten in einem kleinen Rundumschlag einige interessante Neuerungen in C++11 beschreiben. Einiges davon halte ich ja bereits schon an unterschiedlichen Stellen im Buch erwähnt. <06 Stetten*
Mk dem neuen Schlüsselwort auto kannst du auf die Nen- nung des Typs für deinen Bezeichner beim Initialisieren verzich- ten, weil der Compiler nun endlich gescheit genug ist, den Typ aus dem Initialisieret abzuleiten. Etwas nichtiger noch ist das neue Schlüsselwort decltype (ein Operator wie sizeof). mit dein du den Typ eines Ausdrucks ermitteln kannst. Dies kann nützlich sein, wenn du aus einem Ergebnis eines Kontextes nicht genau sagen kannst, was denn zurückgegeben wird. Somit kannst du quasi mit decltype einen Rückgabewert zusam- menbasteln. [Achtung] auto ist nicht mehr das, was es mal war! In alten Zeiten war auto ein Schlüsselwort für automatische Speicherobjekte, des- sen Lebenszeit vom Eintreten in einen bestimmten Gültigkeits- bereich und vom Verlassen dessen abhing. Allerdings waren auch alle Variablen ohne dieses Schlüsselwort immer auto. und somit wurde die Bedeutung in C++11 entfernt. Endlich ist cs in C++ auch möglich, die Standardbehälter über den { }-Inhialisierer mit einer Sequenz von Werten zu belegen. Daniil ist jetzt endlich auch die Initialisierung vereinheitlicht. Dafür besitzt Jetzt Jeder Standardbehälter einen sogenannten Initial isicrcr-l.ist-Konslruktor. Gleiches gilt jetzt natürlich auch für eigene Datentypen. vector<fnt> istneu • {123t345t567,890}; II neu In C++II Ebenfalls neu hinzugekommen Ist in C+*11 die direkte Initialisierung von Datenfeldern in Klassen. Besonders praktisch ist dies natürlich bei Zeigern, um diesen gleich Null (nullptr) zuzuweisen. dass Xyz ( ’1 Eine direkte Dateflfeld-lftitiAli$ierui^£ von Du en bei Klauen «st jetM jb €+♦11 auch e«Liubt. Oer hie« verwendete Hlillptr winde ebenso neu in den C+ +11• } 5 Sfenfard elnßHührt, EinTyp * ptr_EinTyp - millptr; *1 public: £••11 - d«r ntut 607
Die Lambda-Funktionen wurden als neueSprachfunktlon mit dem Hintergedanken eingeführt, kleinere Funktionen an Ort und Stelle einzubauen. Zwar hast du mit inline etwas Ähn- liches. aber da entscheidet immer noch der Compiler, ob er es dann tut oder nicht. Auf den ersten Blick sieht die Syntax dazu ein wenig .schräg" aus. Aber, wie du ja bereits selbst sehen konntest, sind solche Lambda-Funktionen eine willkommene Erleichterung, wenn du STL-Algorithmen verwendest, und auch später bei den Muhlthreads, um einen neuen Thread zu starten. Die etwas schrägere Syntax hierzu: (capture) <paratneter) -> return-type {anweisungen} Zeichen | Bedeutung Dxmit gibst du die Bindung An eine VirrAble im lokilen Be- reich an, [ ] hat fceine Bindung, mit [ ] worden die Werte kopiert, und mit [&] werden die Werte referenziert. () Argument für den folgenden Anwortungsblock (oplKNul) •> Rückgabewert (optional) { } Anweisungsblock mit den Anweisungen Briu.ndteile einer Litnbdi-f unktien *1 Hier wird whr elegant jedes Element do fiehatters vector <hircM*i»fen Nein, es handelt sich hierbei nicht um die Frühstücks-Cerealien. sondern um eine erweiterte Form der f O1T Schleife, die du z. B. von der STL rnit f Or_each her kennst und die wie diese in vielen anderen Sprachen (C# oder Java) schon längst ver- wendet wird. Das Tolle daran ist, dass du dich um fast nichts mehr kümmern musst und dass automatisch vom Anfang bis zum Ende aller Daten gelaufen wird, wie du dies von f or_each her mit den Behältern von b eg in ( ) bis end () kennst. Aber guck selbst: vector<int> vecOl {121,345+567,890}; for(auto 1 : vecOl) { *1 cout « 1 « endl; 608 sictztHh
int Urr(4)-< 1,2,3,5); for(auto 1 : iarr ) { *2 cout « i « endl; > Explizite delete- und def ault-Funktk Du weißt doch, dass dir dein Compiler für Klassen gegebenenfalls den Standardkon- struktor, den Kopicrkonstruktor. den Zuweisungsoperator oder den Destruktor zur Verfügung stellt. Mithilfe von =default und =delete kannst du jetzt auch hier in dieses Standardverhalten cingrcifcn oder es gar aufheben, Sieh dir folgenden Codeausschnitt an: dass Spezial ( int nix; public: Speziaü operator-(const SpcziaU) - delete; SpeziaKconst Speziali) delete; "2 Spezial() default; "3 SpezlaKinc val); void setValfint v) { nix v; } void setVal(double) - delete; *4 H Spezial::Spezial(inc val) { nix-val; } Spezial sl, s2; *3 sl asetVal( L0032); // Compilerfehler *4 s2.setVal(LOO); Spezial $3 sl; If Compilerfehler *2 Spezial s4(s2); // Compilerfehler *1 *1 Dawelbe gilt für den Zuv-risun^vapwatar. der auch hier mit = delete deaktiviert wurde und nicht rmt der Klasse verwendet werden kann. "2 Oer Koplertcofl$uuktor wurde mithilfe vw delete deaktiviert. so dass der Compiler bei der Verwendung die Übersetzung mrt einem Fehler jbtxechen wird. *3 Der Stan-dardkonsvuktor hingegen wurde zum «default erklärt. Dies hat den Vorteil, da« def ault-funktionen wesentlich effizienter smd all die manuelle Version und du dir die Arbeit Sparen kannst, die Funktion manuell zu implementieren. "4 Natudkii kannst du auch Elemcntlunktionen .deaktivieren’. Jede Verwendung van obj.setVal(double) fuhrt jetzt auch zu einer Fehlermeldung beim Compiler In diesem Fall verhindern w.r ganz einfach, dass eine Typkonvertie- rung von double nach int duichgelübrt wird. Endlich gibt cs auch den nullptr, um künftig Zweideutigkeiten mit dem Makro NULL und 0 zu ver- meiden. Denn nullptr kannst du für alle Typen von Zeigern verwenden. C*»Xi - d«r neue 609
Sieh dir folgenden unsinnigen Codeausschnitt an: void nimmmlch(inc 1) {} void nimmmich(int* i) {> • 44 nlmmmlch(NULL); *1 nimnnnich(O) *2 ”1 Obwohl sich hier bei «fern Altlast-AUkro NULL eigentlich ein Nullirigrr befinden will, bcschweft 5lch dc< Compiler, da« dieser Aufruf rwc»dcu1«g «tt rn C>+ w^d Imme* von diesem NULL abgerjrtcn. niinmmichfnullptr); e3 “3 Mit dem nul1pt r ist es jetzt vorbei mit solchen Zweideutigkeiten, und hier weift man sofort, was man aufrufen will. Damit ist endlich eine klare Unterscheidung zwischen □ und einem Nullleiter (nullptrj möglich. *2 Die Alternative wa« dann eben, einfach 0 zu verwenden. Bei einer Verwendung vwi int* val=0 ; ist das kein Problem aber was wallte der Programmierer denn ^etzl bei dem Funktionsaufruf verwenden? Wie erkläre ich meinem Compiler, dass ein Werl konstant ist, der auch wirklich kons* tarn bleibt, obwohl der Wen aus einer Funktion oder einem Konstrukor kommt? Ein einfaches Fallbeispiel: int doubleSizefint val) { return val*2; } M Geht nicht, weil doubleSize(5)+5 ketn mt larr [doublcSizC (5) + 5J; ‘1 konstanter Ausdruck Irtl Und jetzt erklären wir dem Compiler-, das doubleSize(5)+5 zur Kompilierzeit eben doch konstant bleibt: constexpr int doubleS12e<int val) { return val*2; ) Damit wird der Code optimiert und bereits zur Obersetzungszeh berechnet und in den schnellen Lescspcicher gelegt. Damit du eine Funktion mit constexpr dekorieren kannst, darfst du keine void-Funktion verwenden, keine Variable deklarieren oder neue Typen, und cs muss eine Rückgabewertanweisung geben. Ähnlich ist es bei Konstruktoren, welche aus der Konstanten-Initiallsierungsllste bestehen und einen leeren { }-Rumpf haben müssen. Der Destruktor für solche Typen darf dann ebenfalls nur ganz trivial sein. <10 c<0.t«i stceztMW
Ein Konstruktor ruft einen anderen Konstruktor auf Höri sich schlimmer an, als es ist! Du kannst Jetzt auch mit einem Konstruktor einen Konstruktor derselben Klasse aufrufen. Wobei .aufrufen* cs nicht ganz trifft. Es ist eher ein Delegieren. Der Code sollte dies eindeutig rüberbringen: •2 H»cr fleht der Ziellforutrukto* von Callme ( ) dass Callme int a, b; public: CallmeO s Callme(Int If ... ( *1 Der Konctruktor wird delegiert xu Callme(Int,int) Callme(0,0) {} *1 4 x, int y) : a(x), b(y) O ’2 }; Callme mei *3 "3 Hrermit ruht du den Konsuuktor Callme() auf, der delegiert aber gleich weiter ni CalIme(int,int)und Initialisiert beide Werte la.b) mit 0 Move your body Neu in Cetil wurde eine sogenannte Move-Scmantik ein geführt. Das zu erklären. Ist nicht schwer. Du kennst Ja bereits von deinen Klassen her die Kopierkonstruklorcn, mit denen du eine Kopie deiner Klasse erstellen kannst. Manches Mal gibt es aber auch falle, wo du keine Kopie erstellen, sondern nur alles verschieben willst. Hier wird also auf unnötiges Kopieren von Objekten verzichtet, wie dies bei der Üblichen Copy-Semantik der Fall ist. Für diese Zwecke enthalten alle Standardbehälter der STL Jetzt neue Mehioden. welche die sogenannte Rvalue Referenz mit dem Operator && eingeführt haben. Mit && bindest du den Wert nicht nur an Lvalues, sondern eben auch an Rvalttes. Deine bisher bekannten Referenzen mit & können nur mit Lvalues was an fangen. £»•11. - O.r n»u» St*n0*r,J 511
tteud] Rvalues und Lvalues hier nur in ein paar Zeilen abzuspeisen, wäre nicht so toll. Daher empfehle ich dir folgende Weblinks dazu: http://manderc.manderby.com/operators/trvalues/index.phphttp://en. wikipedia.or$/wiki/Vatue_(computef_soence) Um allerdings bei einer Zuweisung dann auch wirklich auf die Move-Semantik operator= (&&) und nicht die Copy-Semantik opcrator=(&) zurflckzu- greifen, wurde die neue Funktion Std : :move ( ) eingeführl. Hier die neue Hove-Semantik im Einsatz: vcctor<int> vecOl { }; vector<int> vec02 vecOl | *1 vector<int> vec03 move(vecOl); *2 *1 Hier wird dir- ubkctie Copy* Semantik verwendet Dm bedeutet. es sind auch zwei gleiche Abbilds im Speicher vorhanden *2 Jetzt die neue Move-Semantik mithilfe der neuen Funktion movc( ). Überflüssige Kopien im Speicher entfallen hierfür NjftMkti bedeutet dies jikK dass der Inhalt van vecO 1 nun leer ist. Hier nochmals Copy-Semantik und MoveSemantik im Vergleich: Bei dei itblichefl Cepy- Semintik findest du zwei Kopien irtft Speicher. Die Move-SernanVk ver lichtet auf uberflüttigei Kopiere«). Natürlich kannst du jetzt auch in deiner Klasse neben dem klassischen Kopierkonstruktor und dem Zuweisungsoperator einen eigenen Move- Konstruktor (Klasse (Klasse&&)) oder einen Move-Zuwcisungsopcrator {Klasse & operator= (Klasse&&))definieren. 512 Capitel SIC*ZtMh
Neues Zeugs Im Einsatz So, in diesem Abschnitt kannst du jetzt mal testen, ob dein Compiler mit dein C++11 Standard was anfangen kann oder eben nicht. auto/decltype *1 Da wir hier cn auto ik Rückgabe wert der Funktion init ( ) 'i'cr^rnden. haben wir dahinter mit dem Pfeil gefolgt vom Typ. vorgegeben, was diese Funktion zurlkicgibt Chief ein int) Als erstes Beispiel will ich dir die neuen Schlüssel* Wörter auto und decltype in der Praxis zeigen: auto initO -> int {return rand()XI00;} *1 2 Dm decltype ßci-spicl ist besonders in^eresvanl.hvcil s*«ir beim FunktiDnvTemp!.it<* nicht w yjer>. welcher Typ hier zmxickgegeber wird im ecrspid übergeben wir hier r w int- und eii*en double Wen. wodurch der Ausdruck dccltypc(x+y) (h w dccltypefint+double)) den Typ double ! 'tert template <typename TI, typename T2> auto multi(Tl x, T2 y) -> decltype(x*y) {return x*y;) *2 auto wasbinichOI 123; *3 auto wasbi.ni.ch02 2.1; *3 vcctor<lnt> vccOI(lO); "3 Hier wird nur auto. ober kein 1yp angegeben. Anhand des InlUiUsterers woßder Compiler selbst, welchen Typ er hier verwenden soll Be» wasbinichOl verwendet er ein int und bei wusbinich02 ein double generatefvecOl.begin(), vecOl.end(), init); *1 forfauto it vecOl. beginQ; itl”vecOl.end(>; ++it) ( ‘4 cout « *it « endl; 1 cout « multf(wasblnichOl, wasbfnich02) « endl; *2 ’4 Besandrn elegant und hcqurm kann dies bvpw. bis der häuf g ums‘.i/idlichcn Schrrihwrlu* von Iteratoren wie hier mit auto it ist..« vector<int>: :congt_iterator itVse.n Anstatt vecOl .begin() bzw. vecOl. cnd( > kannst/ solltest du in 0*11 jetzt (auch) begin(vec01) bzw. cnd(vccOl) verwenden. {} -Initialisieret verwenden Es war schon verwunderlich, dass cs so lange gedauert hat. bis man endlich auch den einfachen verein hei flieh teil { } Initialisie- rer In C++ verwenden konnte. Aber, yeah. Jetzt Ist er auch da! C»*X1 - der neue 613
Hier ein paar Beispiele, wie du diese jetzt in C++11 verwenden kannst: dass FarbeOl { int rgb(3 J; public: FarbeOlO : rgb{255,255,255} {}; *1 }, *1 So kannst du jetzt »n C++11 c«n D.ilen-Array einer Klasse initialisieren dass Farbe02 { *2 *2 Die neue v«sion iw gleichwertig zu Farbe02 färbe (255,255,255) int r, 8» b» public: Farbe02(fnt R-O, int C-0, int B-0) : r{R}, g{G}, b{B} {}; 1 5 Farbe02 färbe {255,255,255}» *2 *3 Veracht den (khaltcf di/ekt nut dem { J-InlUdfclerer mit Werten. vector<int> istncu {123,345,567,890}; *3 int *dynarr - new int|5] {1,2,3,4,5}; *4 multitnap<«tring, string* mp3cdlcction *5 4-.L "Snow Patrol", *'Chasing Cars" }, { "System Of A Down" ''S u gar" } ); ‘4 Auch dynamisch lunkliomert d.n । jetrt mit 0*11 I '5 Und ebenso nut komplexeren Behältern geht das Endirth kann man .ur uivjhlige push_back( ) vertaten!11 Lambda-Funktion Du kennst zwar schon Lambda Funktionen aus der STL, aber ich will dir hier muh ein Beispiel zeigen, wie du lokale Variable darin modifizieren kannst. •1 Mil {(gerade bekommt unsere l.unhd.v Funktion die variable ge rade als Referenz, um diese andern zu können, vector<int> vecOl ” {123,345,567,890}; int gerade « 0; for_each(beg£n(vecOl),end(vecOi),[^gerade] if(!(valZ2)) ++gerade; '2 } *3 ) 9 cout « gerade « " gerade Zahlen\n”; Ohne das Ampersandicichcn würdest du nur einen Wert übergeben. (int val) { *1 “2 Innertiato der Lambda-Funktion wird der Wert von gerade um 1 inkrementiert, wenn der aktuelle Wert, der jetn in der for each-Sthfeife behandelt wird. eineßerade Zahl lil. *3 Das rtt d.st Ende der Lambda Funktion. 614 tipun sicezcxn
Move my own dass Im neuen C*+11-Standard wurden viele Algorithmen und Behal ter für die neue Move-Semantik optimiert, was deinem Pro- gramm zu neuen Performance Schüben verhelfen kann. Ebenso kannst du deine Klassen Jetzt auch damit ausrüsten. Hiermit zeige ich dir ein Minitnalbeispiel: dass Hoveme ( int vall, va!2; public: Hoveme() : Hovctnc(0,0) {} ‘1 Damit implementieren wir den AVwe-KopierkwnUuktar. Movemefint x°0p inu ^-0) : vall(x), val2(y> {} Movenie(M.oven&e4t& m) : vall(m,vall) » val2(m,va!2) { *1 m«valF* tD»val2 • > Hoveme & operator-(Hovemcii n:) ( *2 std: t.wap(val 1, m-vall); std: :swap(val2> m.valZ); return *this; 42 Das isi der Move-Zuweisungsoperator. *3 Mithilfe der Funktion Std: ItDOVef) können w r hier d e Mave-Zuweiwnß Ausfuhren. > void getValf) { cout « vall « " •' << va!2 « endl; > Hoveme meld23, 456); Hoveme me2 n»vc(nicl); "3 '4 Oksgeht hi« jeut nicht melw, weil dadurch, dass du den AVw«-Kopie<lcöns.1»uk!ür und den Move-Zuweisungioperdtor implementiert halt, der SUnd.vdkapiefkonrtruktor Hoveme (const Movemefc} und der Stendarifcuwciwngwpwtar Movetne& operator»(const MovetneS:} intetn als delete markiert sind!" Hoveme me3 im2; // Fehler II! *4 nie 1.getValf ) ; *5 'S Orruß der Mov^-ScniAfltik « me2.getValf); hier jetzt gähnend kj Willst du die Sundardsachen trotzdem verwenden, tw au ehrst du diese lediglich ebenülk zu implementieren. Grummel! Du fragst und beantwortest deine Frage gleich selbst. Probier es doch einfach aus, Schrödinger! Aber ja, so kannst du den Movc-Copy-Konstrukior verwenden. Auch hierfür brauchst du Std: :move( ), um diesen vom Slandard-Copy-Konstnik tor zu unterscheiden. Hoveme me4i (move (me2)) ; - d»r n»ut St*nd*rtf «is
Cool, das neue Zeugs Man(n und Frau) kann wirklich sagen, dass sich die C++Enr Wickler dieses Mal selbst übertroffen und damit zu einem gewaltigen Sprung nach vorne verhülfen haben. Jetzt bleibt natürlich nur noch zu hoffen, dass alle Conipilcrherstclicr diese Sachen implementieren und. last bin not least, dass das neue Zeugs auch von den Programmierern angenommen wird. Mal sehen, ob du die Sache mit den =def ault und =delete verstanden hast. Guck dir die folgenden Codezellen an und sag mir hinter den einzelnen Nummern, welche Zeile eine Fehlermeldung auslösen wird und welche nicht. dass Spezial ( int nix; public: Speziali operator»(const Speziali) - delete; Spezial() = default; Spezial(Inc val); void setVal(int val) { nix=val; } tcmplate<typename T> void setVal(T) delete; >1 Spezial::Spezial(int val) { nix-val; ) Spezial sl, s2; ‘a s1.setVal(1A'); *b s2.setVal(100); *c Spezial «3 sl; *d Spezial s3(a2); *e Spezial s4(100); *f jafipj upj( Q up^ (e (p up^ (o (q U!J1 (e 016 c,c;t«l SICSZCHK
/*» (^t-r ^6*-^ A^K-C 4 void setValdnt val) { nix=val; } teTOplate<typename T> void setVal(T) delete; Damit gibst du praktisch an, dass Set Val ( ) mit nichts ariderem als einem int als Parameter aufgerufen werden kann. Alle anderen Typen lösen einen Compilerfehler aus. Weitere nützliche Features Wir gesagt, der neue C++11-Standard hat schon noch einiges mehr zu bieten, und ich kann es gar nicht erwarten, bei der nächsten Auflage diese Sachen im gesamten Buch elnzupflegcn. Vorausgesetzt, die Compilerhcrsteller haben den Standard bis dahin alle flächendeckend implementiert. Hier noch etwas, das dich interessieren könnte: Statt typedef kannst du eigene Typen auch mit using definieren. Gerade wer schon mal einen Funktionszeiger deklariert hat. wird sich sehr darüber freuen: using INTEGER - int; using FUNC_PTR - double(*>(double); Ebenfalls interessieren könnte dich die neue Bibliothek zum Erzeugen von Zufalls- zahlcn. welche jetzt aus zwei Teilen bestehl: zum einen aus einem Generator (generator engme), welcher die Sequenz erzeugt, und zum anderen aus einer Verteilung (distribution). welche die Zufallszahlen in einem bestimmten Bereich verteilt. Für mehr Infos empfehle ich dir diese Webseiten: http://en.wikipcdia.org/wiki/OX-2B%2B 11 (allgemeine Infos) http://www.openstd. org/jtc l/sc22/wg21/(C** Standards Comlttee) http://cn.cpprcforenec.com/w/q>p (C++11-Referenz) http://www2.research.att.com/~bs/C++0xi:A(2.htmi (C++11-FAQ) “ dor Tf-i.Q Scjflgjra •17
Du meinst Torsten? Ja, der Kerl ist voll nett und hat auch schon unser Buch hier begutachtet. Sein Buch C++J7 /imgnimmreren - 60 Techniken Jurguten C+*11-Code 518 sicaztH*
Nicht nur der eigentliche Kern von C++ wurde mit dem neuen C++11 -Standard veredelt und aufpoliert. Nein, auch neue Bibliotheken und Algorithmen sind hinzugekommen. Neue Behälter für die Backmischung von Dr, STL gibt es Jetzt auch mit array, forward_list, unorderedset, unordered_tnultiset, unordered_map und unordered_multimap. Ebenfalls interessant ist die neue Klasse tuple. mit der du eine Sammlung von unter- schiedlichen Typen, ähnlich wie bei einer Struktur (nur wesentlich komfortabler), verwalten kannst. (HifiKergruHdinfal Als Referenz für die neuen Bibliotheken und Elementfunktionen kann ich dir noch wärmstens folgenden Weblink empfehlen: hltp://en.cppre/erence.com/w/cpp Starten wollen wir mit dem neuen sequenziellen Behälter array<T} n>. Sicherlich fragst du dich Jetzt, wozu noch einen neuen Behälter? Mit vector<T> und einem C-Array decken wir doch schon alles ab! Nein, nicht ganz richtig. Das C-Array ist nicht STL-konform. und der Behälter vector<T> kann nicht für eine statische Größe verwendet werden. Dafür wurde der neue Behälter ar ray<T, n> eingeftihn, was ein Behälter für ein Array mit fester Länge ist und welcher die Vorteile in puncto Laufzeit* verhallen eines C-Arrays verspricht, neben der komfortablen Verwendung von vector<T>. Für seine Verwendung brauchst du natürlich den Header <array>. array<lnt, 5> arr01{); ’1 arrOl.at(O) - 123; arr01.at(4) - 345; forfauto U : arrOl) { cout « it « endl; } *1 Hiermit du rin Array vom Typ int mit eir>cr vtatiwhrn Große vom fünf Elementen an. £••14 • (j«r neu* 619
Mit dem Behälter list<T> hast du Ja bereits einen sehr effektiven Weg kennen gelernt, deine Daten in einer verketteten Liste zu verwalten. list<T> ist perfekt, wenn du eine verkettete Liste benötigst, in der du durch deine Daten sowohl vorwärts als auch rückwärts laufen (iterieren) willst (doppelt verkettete l iste). Benötigst du hingegen nur eine Liste, welche du nur vorwärts durchlaufen kannst (einfach verkettete Liste), kannst du den neuen Behälter forward_list<T> dafür ver wenden. Dieser Behälter ist etwas effizienter als list<T>, weil wir hierzu keinen extra bidirektionalen Iterator benötigen, der ja auch rückwärts laufen kann. Die Anwendung von f orward__list<T> entspricht dann im Prinzip der Verwendung von list<T>. (Zeltet) Unnötig zu erwähnen, dass Element- funktionen wie rbeginO und rend() für forward_list<T> nicht zur Verfügung stehen. Hasch? Ist das Ewig gewünscht und schließlich erhört - nun haben die C+*-Entwickler endlich Behälter für Hashtabellen mit unordered_set. unordered_multisct. unordered_map undunordered_multimap hlnzugefägt. Und Ja. du hast Recht, diese di ii Hi-h.iliem ohne das Präfix unordered_ ihrer Anwendung. Nur mit dem bedeutenden Uni ii die unordered_ Behälter nicht sortiert we hi. h unordered__)ü! Der Zugrifi'auf die cinzc 11 folgt über ein .Streuspeicherverfahren' (Hashing, x । tu dich kannst du hiermit trotzdem Element lür E ti (iterieren). Damit du diese neuen nnst, musst du außerdem die Header <tmo drei set> bzw. <unordered_map> angeben. 620 c«p;t«l SICti?tHh
2 2 *i Wif legen eine neue Math- tabeile an. string istdar Schlüssel und double der Wert using regen unordered_map<string, double*; *1 regen monat; ‘1 nonat("Januar"} 102.23 monat("Februar"] 99.32 aonat. insert(pair<scring, double* ("März’*, 111.32)); *3 monat("Februar"] forfauto 1 : nsonat ) { *5 cout « 1.first « " : 102.33; 4 5 '2 Wir fügen neue Schiüssel’/Wertepaare zur Hasbtabelie hinzu. '3AMt insert() geht« natürlich auch' « i.second « " Liter\n”; *5 > cout « monat["Februar"] « endl; *6 *4 D.is Element wird nicht rtngrfugl. well der Schlussel bereits vorhanden ist. Aber das kennst du )a bereits aus map Pur mehrere gleiche Schlüssel musst du hier den Behäher unordcrcd_multiTnap verwenden ’S Die enwHnen EFemente im BehAHer geben wir ganz modern im C+*11-Stil Jul. Neue "6 So geht cs natürlich auch mit der Ausgabe! Auch ein paar neue Algorithmen wurden hinzugrfügt. In der folgenden Tabelle findest du einige dieser neuen Algorithmen, auf die du sicherlich auch gewartet hast. Was er kann ... allof(itl,it2.pred) Überprüft den Bereich itl bis it2 und gibt true zurück, wenn alle Elemente dem Prädikat pred entsprechen. any_of(itl,it2,pred) Überprüft den Bereich itl bis it2 und gibt true zuruck, wenn mindestens cm Element dem Pradikal pred entspricht. nonc_of(itl, 1x2,pred) überprüft den Bereich itl bis it2 und gibt true zurück« wenn krin Element dem Prädikat pred entspricht. find_if_not(Itl,it2,pred) Sucht im Bereich it 1 bis it2 nach einem Element, welches nicht dem Prädikat pred entspricht. copy_n(it, n, ot) Damit kopierst du TL Elemente aus dem Bereich it in den Bereich Ot move(itl it2, ot) Verschiebt die Elemente im Bereich it bis it2 nach Ot. move_backward(it1,it2,ot) Entspricht move ( }, nur ist die Anordnung in Ot anschließend rückwärts. Iota(itl, it2, val) Füllt in den Bereich it 1 bis it2 den Wert val ein, wobei bei jedem weiteren Element der Wert val um 1 inkrementiert wird. Übersicht über ein* je der neu hiiuugefugten Algonthmen C»BX1 - dtr ntut «21
Tuple? Tulpe? Nein, es ist keine Tulpe, sondern das neue Template tuple ist eher eine Verallgemeinerung vom pair-Template mit mehr als nur zwei heterogenen Elementen. Ein solches tuple (N-Tuple) Ist eine geordnete Sequenz von N Werten, Wie groß N ist. hängt von der Implementierung ab. Vereinfacht kannst du das Template als eine An unbenannte Struktur sehen, dessen Mitglieder eben einfach tuple-Elemente sind. tuple tuple tuple tuple tuple <> nlxj ‘1 <lnt, int, float, double> vierling; «string, int> zwcilingC'Huhu", 123) «Int, int, int, int <string> single; ’5 *1 Das hier Ist em leeres Tuple ohne Elemente 2 *3 int. ine, inc> vlelllng; 4 0 *2 D.11 hier hl ein Tuplr mit vier Elementefi "3 Das Tuple mit zwei Elementen wird auch gleich mit Werten initialisiert. Auch ein einzelnem Element kann man *5 1Ür ein Tuple vetwenden. *4 Hier nun ist ein Tuple mit sieben Elementen. |H«nter^rundinto| Ein solches Tuple kann recht nützlich sein, wenn du eine heterogene Liste mit chiedenen Elementen benötigst, dass du dafür extra eine Klasse oder ne Struktur anlegen willst, um die ^en dort zu speichern. Anlegen kannst du einzelne Elemente direkt mit dem Konstruktor oder mit der Funktion make_tuple (), Der Zugriff auf die einzelnen Elemente kann über get<N> (bezeichner) erfolgen, wobei N wie bei einem Array mit 0 beginnt. Natürlich sind mich die Vergleichs- operatoren (==. ! =, <=. <. > und >=) implementiert. <22 c«oif i sicfftHw
Neue Planeten braucht das Universum Okay, cs wird Zeil, deinen Rechner mit dem neuesten C++11-Compiler anzuwerfen und uns ein paar der neuen Algorithmen anzusehen. Die Verwendung dOrfte eigentlich recht simpel für dich sein. Aber schau am besten seihst. bool zehn(Int val) { return (val > 0 fcfc val < 10); } dass Dlvfsiblc { int div; public: Divi$ible(int d) : div(d) {} bool operator()(int val) const ( return (val I div = 0);} }; vector<int> vec«(2,Ai,6,8}; bool b all_of(begin(vec), end(vec), zehn); '1 if( b trua){ *1 cout « "Alle Werte zwischen 0 und 10!!!\n’'; •1 Gibt true rortek. wenn Atif vec- Wede beim Prädikat zehn() true zxirüt ^gegeben habe«), in diesem Fall also, wenn alle Werte gerade sind. £••41 - e«r r>«u» SC«nOar0 623
“2 Gibt true zurück, wenn mindestens rin Element im Behälter dem Prädikat Divisible(6 ) (aHo tedbar duzch 6) entspricht *3 Gibt nur true zurück wenn kein Element Im Behälter dem Pradik.it Divisiblc(5) entspricht. b anyof(befcin(vec)i end(vec), Divislble(6)); *2 if( b =» true) { ’2 cout « "Mindestens ein Wert ist durch 6 tciLbar^n"; > b » none_of(beginfvec), end(vec), Divisible(5)); '3 ifC b •• true) { *3 cout « "Kein Wert ist durch 5 teilbaren"'} auto it»find_if_not(begingvec),end(vec),Divlslble(5)); *4 cout « *it « endl; *4 Gibt die entc Position »m Behälter zuruck, an der der Wert int carr(10)-{1.2,3,4,5*6*7*8,9JO}; vector <int> vecöl(5); copy_n(carr,5,begln(vec0l)); *5 vector<int> nervig-{5,6,7,8,9,10,11,12,13,15}; vector<int> bessersof11}; iotaibeginfbesserso), end(besserso), 5); ’7 nicht durch 5 teilbar «st iDlvlsiblc(5): *5 Kopiert Fünf Elemente vom C-Array carr an der» Anfang des Behälters vecOl 6 Solche fortlaufenden lnibaliSM?rimgen können recht nervig sein. Noch Irgendwelche Fragen, Schrödinger? "7 Dieser Umsund kann jetzt mit dem Algorithmus iota() behoben werden. Der Behälter wird jetzt vom Anfang ab dem Weil 5 inkrementell Ims zum Ende gefüllt. ’1 Damit vereinfachen wtr anschlie- ßend die Verwendung. *2 Hier stehen zwei neueTuples. die wir gleich beim Anlegen nut Werten versehen. Natürlich kannst du hierzu rin Beispiel sehen. Aber merk dir, ein tuple ist kein Behälter, sondern ein Template! Hier nun ein Beispiel zu tuple: using Tulpe - tuple <int, int, double, string>; *1 Tulpe kontoauszugOl(12, 11* -120*24, "Katzcnstrcv"); *2 Tulpe kontoauszug02(13s 11* -50*25, "Gruner Tee”); *2 int gesamt-get<2> (konxoauszugO l)*gec<2> (kont,oauszug02); cout « ^Gesamtausgaben : ” « gesamt « ” für " « get<3>(kontoau$zug01) M « ” und H « get<3>(koncoauszug02) « endl; *4 43 Hier siehst du den direkten Zugnff auf die Werte mit get<> mit dem Index 2 (datier Wert im Tuple), wo wir du* entsprechenden Werte von kontoauszugO 1 und koncoauszug02 addieren •4 Ähnlich einfach ist es auch mit der Auig.ibe über COUt einfach get<> und den entsprechenden Index angeben 824 ctoLf 1 sicbzcmh
cout « tuple_size<Tulpe>::value « endl; '5 get<3>(kontoauszug02) • "Schwarzer Tee"; *6 "5 Hiermit kannst du dir die* Anzahl der Elemente zunkk^eben hsyen, welche- dein Tuplr speichern kann. if C kontoauszugOl !« kontoauszug02 ) { *7 cout « "Die Auszüge sind nicht gleich! kn11; > Tulpe kontoauszug03; ’B int tag» monat; double betrag; string beschreibung; cout « "Beschreibung cout « "Tag cout « "Monat cout « "Betrag '6 Ähnlich einfach kannst du auch mit goto und dem entsprechenden Index den Inhalt Andern. Im Beispiel ersetzen wir den Text .Grüner Tee* au*, kontoaus ZUgO2 durch den Text .Schwarzer Tee’. •? Alle Arten vw rw Vergleichen mit kompletten Tupics sind auch möglich. i ‘B Hier ist ein neues Tuple, getline(ein, beschreibung) ein » tag; ein » taonat; ein » betrag; kont oaus2ug03"ciaketuple (tag , mona t, bet rag, beschreibung); *9 auto wachtuple • cnakc_tuple( 100, "Gehtso", ’X', 99<99> "Noch nehr”>; MO cout « get<O>(raachtuple) « endl; ’ 9 ... dem wvr alle eingclewiefi Werte mit niake_tuple () direkt übergeben. Das ist wesent- lich komfortable*, ab d‘c e>nzc<nen Werte über get<> zu übergeben, was auch möglich wire. Verwendest du HUtO kannst d-u auf eine Inte der Typen in spitzen Klammem auch verzichten, und der Compiler übernimmt das hie/ für dich /rtC o&i tuple //// d’t<x4_r /tX es j« 4^— / iy£><is fh 3 Natürlich gehl das, du brauchst nur deine Tuples in einen entsprechenden Behälier zu packen. Ich zeige dir das mal anhand des Behälters vector. using Tulpe tuple <int, int, double, string*; Tulpe kontoauszugOl(12, 11, -120.24, "Katzenstreu"); Tulpe kontoauszug02(13, 11, -50.25» "Grüner Tee"); vector<Tulpc> tulVcc; *1 culVec.pushbackfkontoauszugOl); ‘2 tulVec.push_back(kontoauszug02); *2 Tulpe kopie • CulVcc.at(O); *3 *1 ein Vektor für unser Tuplr Tulpe ’2 ... und dH? e-ifsztfnen Tuples hinten hinzutügen 3 Kopie von dem Tuple mit dem Index 0 erstellen d»r Atut St*nd*rö 625
Neue Backmischungen sind auch gut So, jetzt kennst du auch einige neuere Behälter und kannst diese deiner Rezeptsammlung hinzufügen. Wollen wir die einzelnen Behälter nochmals Revue passieren lassen? Itiafich* AutpkJ Nachdem du jetzt auch den neuen Behälter array kennengelernt hast, kennst du jetzt insgesamt drei Möglich- keiten. ein Array in C++ zu verwenden. Da wäre ein C-Array, vector und eben der neue Behälter array Kannst du mir kurz erklären, wozu der neue Behälter array gut sein kann? Prima! Ebenfalls kennst du Jetzt inil f orward_list<T> einen Behälter für eine einfach verkettete Liste, mit der du nur vorwärts iterieren kannst. Der Vorteil dabei ist, dass im Gegensatz zu einer doppelt verketteten Liste wie ÜSt<T> (bei der du in beide Richtungen iterieren kannst) der Aufwand etwas geringer ist und daher ein wenig effizienter sein kann. Auch neu und endlich dabei sind die echten Hashbehälter. die alle mit unordered_ beginnen und keine Nebenwirkung auf das Wohlbefinden haben. An dieser Stelle muss ich noch- mals eindringlich darauf hinweisen, dass die llashbehäher intern nichts mit den gleichnamigen Behältern ohne das Präfix unordered—gemeinsam haben. Auch wenn viele der Ele- mentfunktionen und auch die Anwendung fast gleich sind. Eine Hashtabelle (oder Strcuwerttabelle) verwendet eine speziel- le Index Struktur, um auf die Datenelemente zuzugreifen. «28
Die Behälter ohne das Präfix unordered_ verwenden hin- gegen eine Baumstruktur. Besonders effizient ist das Hashverlah ren beim Suchen von Daten in großen Datenmengen, Dafür gibt cs intern eine mathematische Funktion, welche die Position für ein Darenobjekt einer Tabelle berechnet. Ebenfalls ein interessantes Konstrukt ist das tuple! Wobei tuple selbst keinen neuen Behälter im eigentlichen Sinn dar stellt, aber trotzdem in einem Behälter verwaltet werden kann. Ein tuple ist ideal, wenn man nur pure heterogene Elemente speichern und verwenden will, ohne dafür extra eine Struktur oder eine Klasse zu erstellen. IBelöheturtfc] Prima mitgemacht, Schrödinger! Ich sehe schon, die Neuheiten von C++11 interes- sieren dich riesig. Apropos Tuple contra Tulpe, wie wäre es, wenn du deiner Freundin mal einen Blumenstrauss kaufen würdest? Du weißt doch, dass sie Tulpen liebt I £••11 - d»r A*ut «27
Ebenfalls und endlich hinzugekommen sind die neuen klugen Zeiger (Smart Pointer) shared__ptr. weak__ptr und unique_J)tr. Du findest die neuen Zeiger in der Headerdatei <metnory> (und im Namensbereich std) und solltest sie bei dei- nen Projekten auch verwenden. Die neuen klugen Zeiger können dir dein C++ Leben erheblich vereinfachen, indem sie das Leben der Ressource beobachten, auf die sie ver- weisen. Damit kannst du praktisch eine automatische Speicherverwaltung (Garhagc collection) verwenden. Das Tolle an einem klugen Zeiger ist, dass er sich natürlich nach wie vor wie ein gewöhnlicher Zeiger verhält und dass du hiermit das referenziene Objekt automatisch freigeben kannst, wenn du es nicht mehr brauchst. Drei kluge Zeiger, die sich unterschiedlich verhalten: >* shared_ptr: Das dürfte der wohl klügste Zeiger in der Familie sein, weil dieser dank eines Referenzzählers selber weiß, wann er das Objekt löschen kann. "• weak_pt t: Der wohl „schwächste" Zeiger wird gewöhnlich zusammen mit seinem starken Bruder shared_ptr verwendet, um mögliche zyklische Referenzen auszulösen, die in Verbindung mit shared_ptr auftreten können. "* unique ptr: Er ist ein egoistischer, aber trotzdem kluger Zeiger, der eine Ressource allerdings nur für sich selbst behält und mit niemand anders teilt? Du kannst diesen Zeiger nicht kopieren. Trotzdem garantiert er das Löschen, wenn der Zeiger seinen Gültigkeitsbereich verlässt. IHiAtttgiundn-ifo] Vielleicht hast du bereits schon mal etwas vom auto_ptr gehört, der schon vor den neuen klugen Zeigern seinen Dienst angetreten ist. Den Zeiger kannst du in etwa mit dem unique_ ptr vergleichen. Der auto_ptr war wirklich eine tolle Sache, hatte aber auch so seine Schwachen. Kopierst du nämlich einen auto_ptr, bekommt der neue auto_ptr den kompletten Inhalt des Objektes (eine Art Move-Semantik). Somit konnte der auto_ptr nicht mit Behältern arbeiten und zeigte bei falscher Anwendung auch das eine oder andere Undefinierte Verhallen. Das Standard-Komitee hat daher den auto_ptr als „depreca- ted“ (missbilligt, abgelehnt) gekennzeichnet, und es wird daher empfohlen, in künftigen Projekten stattdessen auf uniquc_ptr zurückzugreifen. Daher geben auch neue C++11 -Compiler eine entsprechende Warnmeldung zurück, wenn du einen autor_ ptr in deinem Programm verwendest, <28 SICCftHh
Folgender einfacher Code soll dir zeigen, warum du auf den auto_ptr verzichten solltest: int* ipOl new int(100); *1 int* ip02(ip01); *1 int aOl - *ipOl; *1 auto_ptr<i.nt> ap01(ncw int(100)); ‘2 auto_ptr<int> ap02(ap01); *2 int a02 = *ap01; ’2 •1 Hier siehst du die klassische Version mit einem int*, über das du Speicher für einen integer reservierst und mit dem Wert 100 belegst. Oer Zeiger ipOl verweist auf diese Adresse AnschiieBend gibst du auch ipO 2 die Adresse, so dass jetzt be«de Zeiger ipOl und ip02 auf dieselbe Adresse verweisen Am Ende übergibst du den Inhalt des Zeigers ipO 1 .in die Inlegef-Variable aO 1 Um die Fre gäbe des Speichen mit delete musst du d.<h aber selber kümmern. shared_ptr<int> int a03 *spOl spOltnew int(100)); *3 spOZ(spOl); *3 •3 ’2 Dasselbe wollen wir jetzt auch mH dem autO_ptr<int> machen imGrundegehenw.rhicf genauso vor wie eben bei unserem klassischen int* Aber durch die Übergabe der Adresse apO 1 an ap02 verhält s*ch der auto_ptr anders. Zwar enthält Jetzt ap02 d i* Adresse auf die zuvor apO 1 verwies, aber apO 1 zeigt jetzt auf einen nicht mehr Der Vorgang nochmals bildlich: definierten Bereich* Selbst eine Überprüfung auf 0 odci- nullptr lässt wh hiermit nicht durchführen. Der Integer a02 erhalt somit von *ap01 einen Undefinierten Wert’ Der UutO_ptr - inn bei Me>irtartuuweiiungtn zu Problemen fuhren, wenn nur» nicM WC»A. W» rrun luUlf *3 Wesentlich besser macht es hier unser kluger Zeiger sharcd_ptr<int> Der arbeitet exakt so, wie wir das von unserem int* her kennen Besser noch, wir müssen uns nicht um das Freigeben des Speichers kümmern, und intern tsl auch gespeichert, dass zwei Referenzen den Speicherbereich verwalten. Aber dazu dann später mehr... i C»*X1 * der neue Sc ander0 62»
Du kannst mehrere shared_ptr (im Gegensatz zuin auto_ptr) auf das gleiche Objekt verweisen lassen. Ein shared ptr löscht somit dein Objekt nicht .aus Ver- "I Wir reservieren Spekher vom Heap. Oer Zeiger ipO 1 bürgt dafür. sehen", wie du dies bspw. mit klassischen Zeigen) machen könntest: *2 ip02 bekommt dlcAnöjpjfctmvor. .3Wlr(^bdn " den Speicherbereich der zehn Integer wieder frin ”4 Bumrtl!’! Hiermit greifen wir auf einen zuvor freigegebenen Speicher zurück, w.is nun mal nicht ertaubt i$L aber teider meidens zunächst anstandslos funktioniert. < int* LpOl = new int(100); *1 int* ip02 - ipOl; *2 delete IpOl;‘3 int aOl = *ip02; M cout « aOl « endl; *4 int* ip03 - new int(123); > *5 "5 Und auch gleich noch ein Spefcheileck. weil ip03 nie mehr .in da* System zurückgegeben wird GiuÄcr bei Programmende). Mit dem shared_ptr brauchst du dir darum keine Gedanken mehr zu machen. Zum einen ersparst du dir das manuelle Freigeben des Speichers mit delete. und der shared ptr hat intern auch einen Referenzzähkn welcher miczlhlt, wie viele Zeiger du eben auf diesen Speicherbereich verweisen lässt. *1 Wir reservieren erneut Speiche« vom Meap, wofür jetzt iinycr kluger Zeiger ipOl bürgt. "2 Auch ip02 bekommt dieselbe Anfangsadresse wie ipOl. ’3 Mithilfe der Elemcntfunktion use_ccunt () kannst du jetzt ermitteln. wie vieM? Zeiger dieselbe Adresse teilen. In unserem Fall sind es jeweils zwei Referenzen. < shared_ptr<int> ip01(new int(100)); *1 shared_ptr<inc> ip02(ipO1); *2 cout « '’ipOl:" « ipOl .use_eount () « endl; *3 cout « ,rip02:" « ip02.use__count() « endl; *3 IpOUresetO; *4 cout « "ip01:w « ipöl.use_count() « endl; *5 cout « ,TipO2:" « ip02.usc_count() « endl; *6 int aOl - *ip02; cout « aOl « endl; shared_ptr<£nt> lp03(now lnc(123))c > "7 ’T Hinter dem Anwcivungsbk>ck wird jetzt der Speicher auto- matisch frcigegcben. weil der Referenzen auf das Spcicherobjckt 0 erreicht hat Auch ip03. wekhes cne Zeile nrw noch emen Speichet reserviert hat, wird hier wieder fre»gegcben, weil hinter dem Anwetsungsblock de* Gültigkeitsbereich endet. 630 sicez(Mk
iz*h«h Damit kannst du praktisch mehrere sharcd_ptr mit demselben Speicher- bereich hantieren lassen, ohne dass diese von der Lebensdauer abhängig sind. Der Speicher wird also immer erst gelöscht, wenn keine Referenz mehr darauf ver- weist und der Referenzzähler 0 erreicht. *4 Mit reset ( ) entbinden wir ipOl vo<n rrfrrrnr^Tfndrn Spef<he<bwKh und letzen ipOl auf den nullptr •5 ipO 1 Kat letzt folne Referenzen (0) mehr und Ist w>cdcr fiel für neue Aufgaben. •6 ip02 hingegen verweist immer noch auf den Speicherbereich. wm der Rückgabewert von 1 auch beweist. (trttrll Einen Tipp habe ich noch für dich. Wenn es möglich ist, kannst du einfach auch auto shptr = make_shared<int>(100); anstatt shared_ptr<int> shptr(new int(100)); verwenden. Die Verwendung von tnake_shared und auto ist häufig wesentlich einfacher und komfortabler zu hand- haben. Derweak_ptr ist eigentlich kein richtiger kluger Zeiger, sondern vielmehr eine Erweiterung für den shared_ptr. um zyklische Referenzen von diesen aufzubrechen. Der weak ptr bietet einen .unsichtbaren* Zugri ff auf einen shared_ptr an. wobei der Ressourcenzähler nicht inkrementiert wird. So gesehen, besitzt der weak_ptr eigent- lich gar keine Ressource. Das Thema hört sich etwas komplexer an, kann aber auftreten, wenn man Referenzzähler eingebaut hat, wie cs nun mal bei shared ptr der Fall ist- Eine zyklische Referenz ist im Grunde nichts anderes als zwei Objek te, die sich gegenseitig referenzieren. Um die .Ressource" eines weak ptr dann zu verwenden, muss diese zuvor mit lock () gesperrt werden, bevor über einen initialisierten shared_ptr darauf zugegriffen werden kann. Ein einfaches Beispiel einer zyklischen Referenz: dass piep ( shared_ptr<piep> plep^; string voegelchenj public? piepfconst string v):voegelchen(v) {) void set_voegelchen(shared_ptr<picp> v) { piep_ - v; ) }; CBBH * d»r ntur •Sl
shared_ptr<piep> piepmatzOl(new piep("Zwitscher”)); sharedjptr<piep> piepmatz02(new piept"Triller")); piepmatzOl->sct_vocgclchen(picpmatz02); "1 piepmarz02->seE_voegelchen(pleptEatz01); *2 *1 piepmatzOl enthält nun juch piepmatzOZ ‘2. ursd piepmatz02 nun u.uch piepmatzOl"1 Was hierauf den ersten Blick ganz normal erscheint, entpuppt sich als Speicherieck. Da beim Löschen von shared_ptr piepmatzOl noch ein zweiter shared_ptr dazu in piepmat z02 existiert, wird piepmatzOl nicht gelöscht. Dasselbe gilt auch andersherum: Wenn versucht wird, piepmat z02 zu löschen, wird auch dieser nicht gelöscht, weil piepmatzOl immer noch vorhanden Ist. Diesem Problem kannst du relativ einfach beikommen, indem du bspw. in der Klasse einen weak_ptr statt eines shared_ptr verwendest. dass piep { weak_ptr<piep> piep_; *1 string voegelohen; public: piep(const string v):voegelchen(v) O void set_voegclchcn(shared_ptr<picp> v) { plep_ - v; > •1 Hiermit lauen lieh zyklische Refererwefl vermeiden. (Achtung] In der Praxis ist es natürlich nicht immer so eindeutig mit zyklischen Referenzen. Daher solltest du bei komplexeren Datenstrukturen deinen Code immer etwas genauer analysieren!*! <32 sletzth*
Der shared ptr Ist zwar prima, aber für manche Dinge brauchst du nicht gleich solch einen Wind zu machen. Geht es dir nur darum, einen Zeiger freizugeben, wenn er seinen Gültigkeitsbereich verlässt, dann reicht häufig auch der unique_ptr dafür aus, weil dieser Zeiger wesentlich weniger Rechenaufwand benötigt. Allerdings ist uniquc ptr sehr egoistisch und teilt seinen Speicher- bereich nicht mit anderen Zeigern. Daher hat der unique_ptr auch keinen Referenz- Zähler. wie dieser beim shared_ptr enthalten Ist. Ein einfaches Fallbeispiel: void leckdenspeichert) { *1 Hier wird Speicher reierviett, aber et EineKlasse* ek(new EineKlasse) ; 1 wurde mal wieder vergessen, diesen am Ende ek->setVdl( 123); mrt delete heiiu^eben. D« kann // Tut Irgendwas mit ek kIw mal passim. II ... viele Zeilen später Und genau für solche .einfacheren' Zwecke wurde unique ptr eingeführt, der bitteschön auch als bessere Alternative zu auto__ptr verwendet werden soll!!! void leckdenspeichert) { unique_ptr<EineKlasse> ek(new ek->setVal(123); II Tut irgendwas mit ek II ... II ... viele Zeilen später > *2 IAH«««] Zwar ist ein unique_ptr sehr exklusiv, was eine direkte Zuweisung, wie bspw. upOl = up02, betrifft (ein Lvalue), aber trotzdem kannst du einen unlque_ptr in einen anderen unique_ptr kopieren, indem du den Umweg über std: :move() (als Rvalue) gehst: EineKlasse)? '1 ’1 Unwr egontiichef Superheld unique_ptr wrgi dafür. . . "2 ... dasi der Speicher am Ende des GültiftkeiUbereicbeS wieder freigegeben wird. unique_ptr<int> upOKnew int(100)); unique_ptr<int> up02=std::move(up01); - d«r ntut St*nd4Fä 633
Klug auch In der Praxis Das Thema mit den Behältern und die dynamische Speicherverwaltung habe ich bis- her bewusst noch nicht angesprochen, weil du hier auf jeden Fall init den klugen Zeh gern besser beraten bist. Im Grunde kannst du zwar recht einfach dynamisch mit new neue Elemente in einen Behälter packen, aber dann musst du dich auch selbst wieder um die Freigabe des Speichers kümmern. Hier ein Beispiel, wie du dynamisch etwas einem vector« Behälter hinzufügen kannst: vector<ELneKlasse *> ekptr02; ekptr02.push_back(new EineKlasse(123)); ekpvr02>pu$h_back(new EineKlasse(345)); ekptr02.push_back(new EineKlasse(678)); & Wie bereits erwähnt, krankt das Beispiel daran, dass man sich jetzt auch noch um die Freigabe von Speicher kümmern muss. Hier kannst du dir mit klugen Zeigern das Leben Jetzt wesentlich einfacher gestalten, Hierzu nun dieselbe Version mit klugen Zeigern, bei der dir die Freigabe von Speicher abgenommen wird: { vector<shared_ptr<EineKlasse» ekptrOl; *1 ekptrOl♦pvsh_back(shared_ptr<EincKlasse>(ncw EineKlasse(123)> > ;*1 ekptrOl.push_back(shared_ptr<EineKlasse>(new EineKlasse(345)));*1 ekptrOl.push_back(shared_ptr<EineKlasse>(new EineKlasse(678))) ;r1 •3 Wrf haben drei Elemente . ‘4 . und zwei Referenzen 5 Wir Kochen alle Elemente Im Vektor. ,, ‘6 ... was uns die BehAlie«- größe auch bestätigt, ... shared_ptr<EineKlasse> spOl = ekptrOl(2]; ’2 cout « ekptrOl.sizc{) « endl; *3 cout « spOl.use_count() « endl; *4 ekptrOl»clear(); ’5 klu cout « ekptrOl .size() « endl; *6 cout « spOl.use_count() « endl; *7 cout « sp01->getVal{} « endl; *1 Düs is» nun d^e kluge Alternative mit sharcd_ptr> *2 Emen weiteren klugen Zeiger auf das zweite Element im Vektor siehst du hier *7 ... aber dank unseres klugen Zeigers haben w>r immer rwh die Ressource geschert und kennen d.imit weiterhin arbeiten! Erst wenn der Rcfcrcnzzabler 0 fest, wird auch d*ese Ressource gelöscht. cph.i siceztHh
Ja, in der Tat kannst du hier so etwas einbauen. Es ist nämlich möglich, dass du für deine klugen Zeiger eine eigene Löschfunktion verwendest. Im folgenden Beispiel zeige ich dir, wie du eine solche eigene Lösch funktion ganz einfach als Eunktlonsobjekt implementieren kannst. Unser Funkilonsobjcki macht neben dem eigentlichen Löschen nichts anderes, als auszugeben, dass eben ein Speicherobjekt zerstört wurde. Hierzu nun ein kluger shared_ptr mit einer eigenen Löschfunktion, um dir das „Wann wird jetzt gelöscht“ besser zu demonstrieren: template <typename T> *1 dass Deleter ( public: void operator()(T* ptr> { cout « "Ein Spcicherobjekt gelöscht!!I\n"; delete per; > }l *1 using EKDcletcr Dclctcr<EincKlassc>; *2 ( vector<shared_ptr<ElneKlasse» ekptrOl; ekptrOl,push_back( *1 Aui unterer eißenen Lcrtchfunktian (unterem Funklionwbjekt) h.ibe ich hier gleich ein vielseitiger verwend (kmes Klasvcn- TempQtf gemacht. Wt wh bet undcren Beispielen verwenden kannst.
shared_ptr<EincKlasse>(new EineKlasse(123) ekptrOl♦push_back( shared_ptr<EineKlasse>(new EineKlasse(345) ckptrOl,push_back( shared_ptr<EineKlasse*(new EineKlasse(678) EKDeleter«))); EKDeleter())) EKDeleter())); shared-ptrcEineKlasse* spOl cout « Hsize() : ” « cout « "Referenzen : " « ekptrOl+clear(); *6 cout - ekptr01(2]; ‘4 ekptrO1.size() « endl; *5 spOlrusc_count() « endl; *5 cout cout > *8 "sizeO "Referenzen sp01->getVal() « endl; •I ekptrO1.size() « endl; *7 spOlrU$c_count() « endl; *7 auch wnn der Schiller jeltwt keine L lernen"'- mehr enthält, so steht der Referenzcnzai: er immer noch auf 1' 3 ’3 3 4 Hn weiteres shared_ptr verweist jetzt auf das dritte Element im Vektor* steht *3 Hier jetzt erneut unsere shared_pt die Speicher für unsere Kusse EineKlasse besorgen, nur dass wir hier jetzt die Löschfunktion (das Funktionsobjekt) mit angetan und somit die Kontrolle selbst in di H.ind nehmen ’S H*er sand noch drei Ekmpn im Behälter, und der Referenzzih auf zwei Referenzen *6 Durch das löschen mit clear () werden jetzt zwei der drei Elemente gelöscht und unsete eigene Löschfunktion aktiv, was die Ausgabe -weh beweist. Es wc nur zwei Elrmrntr tatsächlich c "5 Eist hinter diesem Gültigkeitsbereic wird dtaJi dj^jderenz von *p0,_______________ unseie Loscnfunktion ausgeführt. und aber Das Programm bei der Ausführung: Dank tigert«* Löschfwktion kannst du deutlicher tt kennen. wann etwas geltacM wird. 538 s:cd?(Hh
Bist du auch so klug...? Du hast jetzt erfahren, dass dir solche klugen Zeiger sehr hilfreich sein können. Aber ganz speziell habe ich dir die Vorzüge solcher Zeiger noch nicht beschrieben, was ich an dieser Stelle Jetzt nach- holcn will. Allen voran natürlich die des ultimativen shared_ptr, über den man Ja gar nicht genug sagen kann - einen Zeiger, der clever genug ist, seinen Inhalt freizugeben, wenn er nicht mehr benötigt wird. lElftüchr Aulpb+J Woher weiß der shared_pvr, wann er nicht mehr gebraucht wird? shared_ptr Korrekt! Der shared_ptr ist also ideal, wenn du mehrere Zeiger auf das gleiche Spelcherobjekt brauchst, ohne von der Lebensdauer abhängig zu sein. Ebenso eignen sich die shared_ ptr perfekt für die Behäherklassen! Benötigst du hingegen einen klugen Zeiger mit einer l:1-Beziehung zu seinem Speicherobjekt, dann greifst du auftinique ptr zurück unique_ptr wurde auch als trsaiz für den „deprecated" auto ptr eingeführt. Der unique ptr eignet sich bestens für die automatische Freigabe von lokalen Objekten oder Klassen daten. bei denen man schnell mal ein delete vergessen könnte. Ein paar Dinge solltest du auf jeden Fall bei der Verwendung von klugen Zeigern beachten. Das Allerwichtigste ist natürlich, dass du hierfür zunächst ein mit new erzeugtes Speicherobjekt einem solchen klugen Zeiger zuweist. Künftige Aktionen solltest du nur noch über diese klugen Zeiger durchführen! Ebenso kannst du nicht direkt einen _ptr<T> einem T* und umgekehrt kannst du kein T* einem __ptr<T> zuweisen. Solche Aktionen kannst du nur explizit machen. Den .normalen' Zeiger von einem _ptf<T> kannst du mittels get ( ) haben, und aus einem T* kannst du mit _ptr<T> (T*) ein _ptr<T> machen. • d«r neu« 637
Ein (negatives) Beispiel hierzu: int *iptr = new int(100); uniquc_ptr<int> upOl(lptr); *1 int *ptr - upOl.gecO; *2 *1 Explizite Umwandlung von int* xu unique_ptr<int>. . *2 .. und hier passiert genau das Gegenteil! {Achtung! Trotz alldem, da« du hier auch explizit zwischen _ptr<T> und T* hin- und herspringen kannst, solltest du, wenn möglich, die ganze Arbeit nur mit klugen Zeigern verrichten, also ausschließ- lich über den klugen Zeiger referenzieren. Nur so kannst du dir sicher sein, dass ein Speicherobjekt nicht versehentlich gelöscht wird!!! |(irtUih* Aufgabe] Was sind .zyklische Referenzen*, und wie kannst diesen begegnen? <38 c<p;t«l siccttww
Bibliotheken für die regulären Ausdrücke gab es zwar schon einige, aber es war halt immer lästig, wenn man sich bei der Portierung vom Quelltext auf andere Systeme damit herum- -i.----. schlagen musste. Jetzt gibt cs mit dem C++11-Standard endlich auch standardmäßig die regulären Ausdrücke Im Angebot. |AbUfe) Zur Drucklegung des Buches konnte Ich nur mithilfe des Visual C++ 2010 und dang 3,0 die regulären Ausdrücke übersetzen. Der GCC (hier Version 4.7) unterstützte die regulären Ausdrücke noch nicht korrekt, clang ist sowohl für Linux, Mac OS X als auch andere Systeme verfügbar. Reguläre Ausdrücke (cngl. regulär expres- sions) sind eine formale Sprache, mit der du eine (Unter->Menge von Zeichen- ketten beschreiben kannst. Solche regu lären Ausdrücke sind dabei keine besondere Funktion, sondern eine echte Sprache mit einer formalen Grammatik, in der jeder einzelne Aus- druck eine feste Bedeutung hat. Die Anwendung ist sehr vielseitig und wird sehr gerne in Texteditoren oder anderen Programmen ver- wendet, um nach einem bestimmten Muster im Text zu suchen und dieses dann durch etwas anderes zu ersetzen. Die regulären Ausdrücke sind enorm mächtig, und deren wirk- licher Macht dürfte diese Einführung kaum gerecht werden. Mir gehl es nur darum, dass du zumindest einfache und gängige 83®
Ausdrücke selber formulieren kannst oder zumindest versiehst, was das Gekritzelte bedeutet. Du erfährst also nur Grundlegen- des dazu! Aber als Freund des Web 2.0 dürftest du auch schnell tiefgründige Informationen dazu finden. IHmtergruridintat Die Grammatik von regulären Ausdrücken ist ebenfalls spezifi- ziert. Der C++-Standard verwendet hierbei die EMACScript- Grammatik und dürfte somit die bestmögliche Grammatik dar- stellen (insgesamt gibt es hier sechs verschiedene Grammatiken). Als Zeichen oder Zeichenliterale bezeichnet man die Zeichen (wer haue das gedacht), die wörtlich genommen werden und im regulären Ausdruck direkt notiert sind. Neben den gewöhnlichen Zeichen findest du auch noch sogenannte Metazeichen, welche logischerweisc eine besondere Bedeutung bei den regulären Ausdrücken haben. Diese Metazeichen sind: In der folgenden Tabelle findest du einen Überblick zu gängigen Metazeichen: 1 Mctazcichcn j Sinn und Zweck j' (1 Auswahl von Zeichen, bspw, [abed] o Definieren von Teilausdrücken o 1 Wiederholungsangabe, bspw. {3} ome Alternative definieren, bspw. äbed | ABCD ? Wiederholung (null oder einmal), bspw. a? + Wiederholung (mindestem einmal), bspw. a+ — Zeichenberekh definieren, bspw. | a-z ] ★ Wiederholung (beliebig oh), bspw. a* $ Zeilenende, bspw. cndc$ A ZeilenanUng. bspw. *anfang \ Metajeiche« schützen, Zeichenklawcn einleitcn oder Rückwartsreferenj einleiten . (Punkt) bcheb<ges Zeichen (außer Zeilenendezeichen) Venthrfdene Metutichen und der-ea Bedeutung <40 CiM.I SICtftMH
Die Zetchenauswahl definierst du zwischen eckigen Klammern [ auswahl ]. Alles, was du in diesen Klammern schreibst, gilt dann exakt für ein Zeichen dieser Aus- wahl. Beispielsweise gibst du mit (xyz J eines der Zeichen *x«, »y» oder »z« an. So etwas kann auch in Bereiche au fge teilt werden. Somit steht [ a* Z ] für den Bereich der Kleinbuchstaben »a* bis »z«. während du mit (5-9 ] die Ziffern 5 bis 9 angibst. Willst du diese Zeichen ausschließen (negieren), brauchst du einfach das Zeichen A davorzusetzen. Mil ( *0-9 ] schließt du alle Ziffern von 0 bis 9 aus, Anstatt eine solche Zcichcnauswahl, wie bspw. (a- zA-Z0-9_), zusammen- zubasteln, gibt es glücklicherweise auch vordefinierte Kurzschreibweisen dafür. die sogenannten Zeichenklassen. Bezogen bspw. auf [a-ZÄ-Z0-9_] wäre \w die vordefinierte Kurzschreibweise dafür. In der folgenden Tabelle findest du weitere Kurz Schreibweisen. I Zeichenklasse | Zweck I Selbst definiert \d Ziffer von 0 bis 9 I0-9) \D keine Ziffer von 0 bis 9 ro-9i \s Whiteipace-Zeichen (\f\n\r\t\v] \S kein Whitrspice- Zeichen [*\f\n\r\tVv) \w Buchstaben, Ziffern und Unterstrich |a-zA-Z0-9_] \w keine Buchstaben. Ziffern und kein Unterstrich [*a-zA-Z0-9_l KuriKhreibweisen der einzelnen Z<i<henkfa«en muhen du Leben leichter *♦# (Hintrrgnindintal Zu diesen Zeichenklassen gibt es noch einige speziellere Zeichenauswahlen dazu, welche da noch etwas spezifischer sind, um bspw. auch zwischen Groß- und Kleinbuchstaben zu unterscheiden, wie etwa [: upper: ] und [: lower: ]. Dann fehlen natürlich noch die Zeichen für Wiederholungen (auch Quantoren genannt), mit denen du bestimmst, wie oft der Ausdruck in einer Zeichenkette Vorkommen dari/soll. In der Tabelle findest du die Bedeutungen der einzelnen Zeichen. - dir ntut «41
| Quantifizierer | Was es bedeutet... ; Der vorangehend«* Aufdruck ist optional. Er kann einmal Vorkommen, muss es aber nicht. Der Ausdruck kommt entweder null- oder einmal vor. + Der Ausdruck muss mindestens einmal Vorkommen, darf aber auch mehrmals vorhanden sein. * Der Ausdruck kann beliebig oft Vorkommen, {n} Der vorangehende Ausdruck muss genau n-mal Vorkommen. {tnin.max} Der Ausdruck muss mindestens Dlin mal, darf aber nur maximal na x-mal vorkommen. {min,} Der Ausdruck muss mindestens snin mal vorkommen {,max} Der Ausdruck darf höchstens maX-mal vorkommen. Wiederholung«* für detae Aufdrucke Ausdrücke können auch zwischen runden Klammem gruppier! werden. Damit speicherst du diese Gruppierung ab und kannst diese im regulären Ausdruck bzw. in der Texterseuung über \ 1 wiederverwenden. Du kannst hiermit bis zu neun Muster abspeichern <\ 1. \ 2. \9). Zürn Beispiel würdest du mit "(stringlU Ustring2\) \(string3\)/\3 \2 U" erreichen, dass alle Vorkommen von string 1 string2 string3 umgeänden würden in string3 string2 String 1. \ 1 bezieht sich also immer auf das erste Klam* mernpaar. \ 2 auf das zweite usw. Selbstverständlich lassen sich hierbei auch Alternativen defi- nieren. Hierfür wird das Zeichen | verwendet, wie bspw. (asdf | ASDF) bedeutet, dass hiernach asdf oder ASDF gesucht wird, nicht aber nach As Df oder asdF. <42 i stceztHK
*1 Aufdruck direkt beim Anlegen des Objektes übergeben De* Ausdruck überprüft, ob nur Zeichen zwischen j bs z und A bis 2 eingege ben wurden. *2 Ah C'String ist d.n ebenso wenig n n Problem .. *3 ... wie als string Um einen regulären Ausdruck verwenden zu können, brauchst du zunächst die Header- datei <regex>, welche diesen Ausdruck speichern kann, und dann natürlich auch einen Typ dafür. Selbstverständlich stehen alle Sachen auch hier wieder im Nainensbe- relch std. Der Typ daftir lautet Std: : bas ic_regex. Per Typ ist ein Klassen- Templatc und somit auf alle Charaktere von Zeichen spezialisiert (char*. string = std; : regex; wchar*. wstring std: xwregex). Du kennst das ja bereits von Std: :basic_string. Du kannst also solche Objekte folgenderma- ßen erstellen: const char* cexpr "(a-2A-Z)+"; string sexpr(,,[a-zA-Z]+’,); regex regOl (" [a-zA-Z] +*'); *1 regex reg02(ccxpr); '2 regex reg03(sexpr); *3 regex reg04{ ”^d {4}"); '4 regex regO5(R'’(\d<^>)">? *5 *4 Hnirmit überprüfen wir, ab der Aufdruck aut vier Dezi mal zahlen brtfeht. Damit d.w Metb Zeichen \b nicht als Escape-Sequenz vom Compiler betrachtet wtrd. müwn wir cs mit einem BacksUsh- Ze*chen davor schützen (daher \ \bl 5 0*11 hat einen sogenannter iRaw-String -Nngciuhft (dec hker mit R beginnt), welcher empfohlen wird, künftig bei regulären Ausdrücken zu verwenden. Oer Raw-Strlng macht d»c Jache mR dem \ wesentlich einfache/, und du kannst au< doppelte \ \ verzichten. Der beste reguläre Ausdruck hilft dir nichts. wenn du keinen Algo- rithmus dafür hast, der das Ganze verarbeiten kann. Hierfür ste- hen dir folgende drei grundlegende Algorithmen zur Verfügung. |Zell«t] Zur Drucklegung hat die Sache mit dem Raw-String nur beim GCC (4.7) funktio- niert' Aber diese Version kann noch keine regulären Ausdrücke. Visual Studio 2010 und dang hingegen konnten bis dahin reguläre Ausdrücke, aber keine Raw-Strings. Algorithmus | Was er kann ... std: : regeX—iaatch Überprüft. öb «in Slring dem regu- lären Ausdruck entspricht. std::regex_search Sucht nach dem regulären Aus- druck in einem Text. std::regexreplace Damit kannst du jedes Vorkommen des regulären Ausdrucks durch einen String ersetzen. Die dre-i klAHifthen AlgatiiKmen für regullrt Auidrücht C*all - der ntu« 643
(HinCfttrundinM Essollte natürlich nicht unerwähnt bleiben, dass alle Algorith- men mehrfach überladen sind und ich dir das Thema hier nur für den einfachen Hausgebrauch demonstriere. <z»u»n Zum wiederholten Suchen findest du mit «ed:: sregex_iteratet und std::sregex_token_iterator zwei wirklich beeindruckende Werzeuge, mit deren Hilfe du die Vorkommen von regulären Ausdrücken in einem String iterieren kannst. Suchergebnis bür alle Algorithmen findest du auch eine Version mit dem Parameter Std : : smatch, worin das Ergebnis der Suche enthalten Ist. welche du für eine Analyse bzw, Auswertung deiner Suche verwenden kannst. Hierfür stehen dir viele Funkti- onen zur Verfügung, wie bspw. die Anzahl der Gesamt treffer, Position. Teiltreffer, Lange usw. Solche Teilmuster bzw. Erlässungsgruppen (capture groups) werden, wie du ja bereits weist, mit den runden Klammern defi- niert. Das Gesamtergebnis findest du dann in smatch [ 0 ]. die weiteren Erfassungsgruppen in smatch [ 1 ].
Suchen mit Hieroglyphen Als erstes Beispiel wollen wir einen einfachen String mit einem regulären Ausdruck prüfen. So etwas kann z. B. sehr nützlich sein, um eine Benuizereingabe zu überprüfen. Und keine Sorge, ich belasse es zunächst bei einfachen regulären Ausdrücken, so dass du dich erst mal mit den Funktionen auseinanderselzcn kannst. •1 Den kennst du ja petzi vchan’ Nur die Zeichen a bis z und A bk Z sind erlaubt. ITipp] Falls dein Compiler noch keine regulären Ausdrücke kann, kannst du auch auf die regulären Ausdrücke der Boost- Bibliothek zurückgreifen, weil es sich beim neuen Standard im Grunde nur um die Aufnahme dieser Bibliothek handelt. Hierbei musst du eben nur die Headerdatei auf <boost/ regex.hpp> ändern und den Namensraum boost verwen den. Natürlich will dein Compiler beim Übersetzen auch den Pfad zu dieser Bibliothek wissen {also bitte verlinken)!!! regex exprOl (h[a-zA-£]*’‘); *1 regex expr02(; *2 II .besser wäre die Raw-String-Variante: rege* exprO2(Rl,(\d(4i})tt>; *2 string text, PIN; "2 Der Ausdruck erwartet exakt vier Dezimal- jittem. Wenn dein Coinpilei vchun Rdw-Stringi kann, empfehle ich dir, dieie vtattdewen ju verwenden. cout « "Bitte nur Buchstaben eingeben: getlinefcin, text); if( regexjruitch(text, exprOl) •• true ) { *3 *3 O»e Suche gibt true zurück. cout « "Pefekt!!!\n"; wenn deine Eingabe in String text dem Ausdruck exprOl entspricht Ulmd nur Zeichen van a biv z und A biv Z enthält). eise { cout « "Bist du nicht bei der Sache (lol>7??\n"; > cout « "Bitte vierstellige PIN eingeben: ”; ein » PIN; if ( regex maecht Pitt, expr02) true ) { '4 cout « "Pefekt!\n"j > eise { cout « "Was stimmt mit dir nicht???\n"; '4 Hierbei wird nur true zurückgegeben, wenn m PIN vier Dezimahiffetn enthalten sind. C*«1X - c»r n«u* St«nd«rd 645
ICocfe bearbeiten) Ich dachte mir schon, dass du dich daran au (hängst, daher auch das (lol) hier. Beachte, dass auch ein Leerzeichen nicht zur Kategorie a bis z bzw A bis Z gehört. Aber das kannst du ganz einfach ändern, indem du den Ausdruck wie folgt schreibst: ” [ a-zA-Z ] +" (hinter dem Z steht hier ein Leerzeichen). Das Programm bei der Ausführung: Dai PregriMm bei der Am Führung Reicht dir die Überprüfung nach einein einzelnen String nicht mehr aus und suchst du nach einer weiter greifenden Suche., um bspw. eine komplette Datei nach einem regulären Ausdruck zu durchforsten, dann ist regex_search() sehr gut dafür geeignet. Im folgenden Beispiel wirst du nach einer Datei gefragt, die zum Lesen geöffnet werden soll. Unser regulärer Ausdruck, den wir hier verwenden, versucht, alle E-Mail-Adres- sen aus einem Text zu filtern. |rc-ocij| Mehr Informationen, wie du eine gültige E-Mail-Adresse mit einem regulären Ausdruck darstcllen kannst, findest du hier: http/Avww, regular-expressions.mfo/enuit.html 646 Giltn SIC«Z(HH
Mehr such, such: *1 liier üteM. d«r reguläre Ausdruck, de» eine E Mail Adresse ausdrtkcen wll. regex cmailcxpr( "(Uw-]+(?t\\.[\\w-]+)*e(?t[\\w-]+\\.) + (a-2A-Z){2,}"); *1 ... besser wäre die Raw-String-Variante: regex enwiilexpr( R"((\w-]+-(?:\. [\w-]+)*®(?: [\w-]+\\. )+(a-zA-Z){2,7})”); datei, puffer, linc; ‘2 '2 Hier kannst du die Ergebnisse rteinrr Gliche au-werten. If If string suche, fitnatch match; cout « "Welche Datei willst du durchsuchen: "j *3 gctlinefcin, datei); ifstream rstreata( datei. c_str()) ; if( Irstreani ) { cerr « datei « " konnte niih^keöf fnet werden Kn return 1; > ll "3 Hier g bst du die Datei ein in der wir suchen wollen. stringstream str; ’4 while( gctlinc(rstream, linc) ) ( str « Line « endL; > puffer - str.strt); "4 Den kompletten Inhalt der D.itrj whic-hcn wir in r nen s tr Ings tream und übergeben den nnaft dann an den String pxi f f e t. wo sich jetzt der komplette Inhalt befindet string::ccnSt_itcrator it 1 - puffor.beginf); *5 string::const_iterator it2 = puffer.end(); *5 while ( regex_aearch(itl, it2, iratch, emailexpr} ) { *6 r( auto c : match ) { if(c.matched) { *7 cout « c.str() « cout « c.lengthO } itl-c.sccond; *10 ‘5 Für die nnschliesende Suche mit regex_ Scatch ( ) kannst du den Such bereich mit Ite'dtoren einschränken. Wir wollen im kompletten Te*t von beginf) bisend{) suchen. Alternativ kannst du hier auch auto itl=puffer. cbegin()und auto £t2=puffer-cend(> stattdessen verwenden Das erspart dir string;: •const— itertor * T Wurde nrwa; gefunden. ’B ... geben wir hie- die Zeidierkette zjrüuk ... "9 ... und hier dir I an^r der Zeichenhefte « : *8 « '* ZeichenVn"; '9 ‘10 Damit wir um nicht in eirer Endk>»chkife verlieren, müssen wir den Iterator itl aufd,? Position des »letzt gefundenen Zp- rnpn$ (seCOIld) Wtzen (Jen Anfang der Fundstelle wurdest du mit first (c. first) erhalten. Cmü - "6 Jetzt slditen wird die Suche mit regex_search() MH itl gibst du die Aniangs und mit lt2 die Endposition .in, wa du nach dem Auwlnjck emailexpr suchen wllWt Bei erfolgreicher Suche wird true zuriiekgegeben (ansonsten false) Mehr Infu-niati'unun zur Suche werden in mat ch gespeichert. neu« Stander^
Das Programm bei der Ausführung: * y jhj 0 14 0' IJ 1$ $0 41 pjt*i v -i4>«um<nfi.ltuch»ri^<hr<i rxjskjipt^lr^i/ÖO? ,'tfrxt tvl * ► • Ifrt hit I ir$ vfmW wlicr:- t Auch 9lbt es 4er ftn Schwerz or sich hebt. in denen frühen’ schroedinoer^schroedir^er .de sucht weil er Schme^ ist, es sei dem, i Schwerz thw curt? nocheeme.de oder wünscht, nur, ker-wt zu zufälligen Ueatfinden, le Freude bereiten körnen. m ein triviale! Beispiel «er von uns unterzieht sich je ortStrenpeiMeryorperliyfer aewiifrz^. außer vn vorteile daraus zu Heben7 Aber *fer hat Jtoend ein Recht. «eirteMoiitprovider.co« einen Menschen zuYaöeljf, der die Lntsc/^ldurq trifftr eine Freude zu QtnieÄen, die krtneXjnanqer^r<jjzf*6lgen höt. oder einen, der Schmerz vermeidet, welcher k\tp’ daro-•xÄultlercnde Freude nach sich zieht? wer von uns ifiterztqht v.cm. ouFrif utfi Vorteil* ju eine« Menschen zu tfl«f\^er die keine unor^enetw^n föibeß^ätyb»^ welcher iteine daraus resultier^hdcf reu Auch Gibt es mewder>jLj«?n/d<n «khawrjidfr Mehr such such .. nur, eil er s<tnerz tsV»s sei denn, Schroedinger $ ./main in deren Kjhen jnd Sch-ertl irnarofie fr M , . r . . - , , , , _ wct^ine^wmiadresse.atV.^^ia Welche Datei willst du durchsuchen: text.txt ^chroedinger^schroedinger«de : 28 Zeichen Hochfeine,de : 12 Zeichen meineMail^provider^con : 22 Zeichen suchmeine^enailadresse.at : 25 Zeichen Xn^,:Xr“"’'wtÄ*hierbinichS>verst«kt.n.e : 23 Zeichen q Schroedinger $ ?'l 1 C4« : UnKode iUTF Bi I L^ > IL< Ofden!lieh gctrlAig, untrrr Su<hr mi| rC£«X_»e«Tch( } ’1 Damit werden alle Zetchm zwischen puff er. begin() ins puffer. end ( ) itefiert und nur die Zeichen zurückgcgebcn. welche dem Ausdruck cmailcxpr entsprechen. Hiermn werden ebenfalls alle E •Mail-Adressen aus dem Text flcfiltert. Hur wesentlich kqmfortAbkr. Sicherlich findest du in dem Beispiel die Verwendung von regex_search() mh dem itl=c. second rcchi umständlich. In diesem Beispiel sehe ich das auch so, und du kannst es dir hier wesentlich einfacher mit der wiederholten Suche mit dem Werkzeug std::sregex_iterator machen. So kannst du bspw. die while-Schleife aus dem Beispiel zuvor durch folgendes gleichwertiges Konstrukt ersetzen: ♦ ♦ fr sregex_lcerator it3( puf fer.beginO , puffer.end(), emailexpr ); "1 $regex_Uerator it4; while( it3 !- it« > { cout « *it3++ « endl; *1 } <48 i sicertHW
Natürlich. hierzu musst du aber den Ausdruck zunächst in einzelne Erfassungsgruppen au fiel len. Du weißt ja noch, hierzu sind die Klammern im Ausdruck nötig. In diesem Beispiel kann das recht knil llig werden. Der Ausdruck würde demnach wie folgt aussehen: rcgcx cmailexpr("([\\w-]+)((?. [Uw-]«)*£)((?: [\.)) + )Ua-zA-Z] <2,})"); Die einzelnen Teile aus den Klamineningen des Ausdrucks findest du Jetzt In smatch [ 1 ] bis smatch [ 5 ] und könntest du wie folgt durchlaufen: while( regex_search(itl, it2, manch, emailexpr) ) { for( auto c : match ) { if(c.matched) { cout « c.str() « endl; > itl»c.second; > > Jetzt will ich dir noch das Suchen und Ersetzen mithilfe von regex_replace ( ) demonstrieren. Und um das Thema einfach zu halten, wollen wir jetzt nur noch ganze Wörter als Ausdrücke verwenden. Hier das Beispiel für ein Suchen und Ersetzen: II Datei öffnen, wie gehabt im Beispiel zuvor string suche, ersatz; regex expr; cout « "Was willst du ersetzen: *1 getline(cin, suche); *1 expr = *'(” + suche + *1 cout « "Gegen was: *2 getline(cin, ersatz); '2 string new_text; *1 Sucfatrinß eingeben und .in regex expr ubcfgebrn *2 Ers.itztfririnuebcn - der ntue 649
regcx_replacc( back_inserter(new_tcxt)> puffer«begin()» puffer-end(), expr, ersatz)5 ’3 cout « new_text « endl; *4 •4 Oe Ausgabe «st der Beweis Das Programm bw der Ausführung * *2* ______ S*CXft ÜMtM« Schroedingcr $ ,/wain •• Reiche Datei willst du offnen: text.txt Nos willst du ersetzen Prooram Gegen ms; Sograwi Wer tastet sieh nachts die Finder klamm? Es ist der Sogreinerer mit seinem Sogram! Mein Meister. rein Meister, horst Du dos Grollen? Die wilden Bits durch das Extended tollen! Nur ruhig, nur ruhig, das hoben wir gleich, die sperren wir in den Pufferbereich, Er tostet und tastet wie besessen, Scheisse > jetzt hot er zu saven vergessen» der Sograjhuerer schreit in höchster Qual, do zuckt durch das Fenster ein Sonnenstrahl. Der Bildschirm schimert i- Morgenrot. Sogrorrr- gestorben, Sograw-terer tot !!! j * Schroedingcr S *3 Wir suchen im Bereich puffer.begin() bl$ puf f er . end( ) nachdem Ausdruck expr und ersetzen diesen durch ersatz Die Ausgabe des kompletten Textes erfolgt in den String ncw_tcxt über den Iteratorenamau milhiHe von back_inserter() < ► cext ui : Uw lymbol stlectt« Tag; Einnahmen; Ausgaben Montag; 1ZM;ZM5 Dienstag; 3Z12; 2321 Mittwoch; 12 S4; 2342 üonfwrstag;S173;’.M3 Freitag;.^234; 1234 lexi.Lxi LM'Uvwd: IS.0M2 IS 1354 Filt F^h v ; -,lChxi^-r^nl|.l kKhf«(lSchro 550 sicaztH»
Ja» hier könnte ich dir sregex_token_lterator wie folgt empfehlen: *1 Hier rjit unw .Takdn“, anhand dessen wir regex expr ( **; tt) ; *1 unseren Str«ng zerlegen lassen. fl Datei zum Lesen öffnen, wie gehabt ‘2 Wir leien zeilenweise aus dem Datenstrom rstream in den Stnng line ein while( getline(rstream, line) ) { e2 sregex_token_itcrator it1( line-beginf), Hne.endO, expr, -1); *3 sregex_token_iterator it2; while ( it t , it2 ) { 4 '3 Jetzt zedegen wird anhand des .Tobens’expr den Inhalt cout.width (10); von line,bcginO bis line,end() cout « « ”\t,,/4|^^ cout « endl • Hier durchlaufen wir den zedegten Bereich und geben diesen auf dem Bildschirm aus. Und das macht unser Code jetzt aus der Datei: ann CSV-DMti JAjItrtrtrttft Schroedinger S ./irain Welche Datei willst du öffnen: text.txt Tag Einnahmen Ausgaben Montag 1234 2345 J Dienstag 3212 2321 1 Mittwoch 1234 2342 \ Donnerstag S123 1343 J Freitag 3234 1234 J Schroedinger $ < **-*-*-* ^*
Cleopatra Ist da Die regulären Ausdrücke sind wirklich eine feine Sache in C++, und ich möchte dich nochmals ausdrücklich darauf hinweisen, dass wir hier nur die Spitze des Eisberges behandelt haben. Ich empfehle dir. dich auf Jeden Fall mehr damit zu beschäftigen. Es lohnt sich wirklich! Zunächst lohnt es sich natürlich, sich mal intensiver mit den regulären Ausdrücken im Allgemeinen zu befassen. Aber auch wenn du mit den regulären Ausdrücken nicht vertraut bist, findest du im Web unzählige „Rezepte", die du zum Teil ohne große Änderungen für deine Projekte verwenden kannst. Eine interessante Sammlung von Anregungen findest du u.ä. hier: http://www.regular-expressions.infa/exampies.html (MoliJl Um die regulären Ausdrücke intensiver zu erlernen, musst du 4 nicht unbedingt ein Such kaufen. Wenn du mit Google umgehen kannst, dürftest du auch hier mit unzähligen Tutorien ziemlich weit kommen. Um einen regulären Ausdruck dann in C++ benutzen zu können, verwendest du hierfür das Klassen-Template Std: : basic_tegex oder, genauer, eine der Spezialisierun- gen wie std: : regex oder std: :wregex. L. E lÄchlun^l Im Fall eines Fehlers innerhalb der Bibliothek der regulären Ausdrücke wird die Ausnahme rcgcx_error geworfen. Für so etwas solltest du dich auf jeden Fall rüsten, falls du dem • ' Anwender erlaubst, eigene reguläre Ausdrücke einzugeben. Mithilfe eines solchen Objektes kannst du dann einen der Algorithmen std::regex_match(>, std::regex_search(). std: : regex_replace ( ) oder für wiederholtes Suchen die Iteratoren std: : sregex_iterator bzw. std: : sregex_token_iterator verwenden. Dass die Algorithmen mehrfach überladen sind, habe ich bereits erwähnt. Zusätzlich kannst du den Algorithmen noch spezielle Flags für weitere Suchoptionen vom Typ std::regex_constants übergeben. <62 sicartMH
Um die Suche gegebenenfalls auch noch zu analysieren, stehl dir std: : match_results zur Vertilgung, was du ja mit dem Typ Std: : smatch im Listing verwendet hast. Nicht erwähnt hatte ich bisher, dass std: :match_results ein sequenzieller Behälter ist und dass die einzelnen Erfässungs- gruppen wiederum als std: : sub_match Objekte zurück- gegeben werden. Beide Objekte wiederum bieten unterschied- liche Funktionen an, um das Suchergebnis zu analysieren. Das folgende Listing soll dir das Thema nochmals etwas schmackhafter machen: string ip("192.168.178,1’'); regex expr("(25[O-5)|2(0-4][0-9)|[01)?(0-9](0-9)?)\\." *1 "(25(0-5)(2(0-41(0-9)|[01]?(0-91(0-9)?)\\." "(25(0-5)|2(0-4J(0-9)|[0(1?(0-91|0-9)?)\\." "(25(0-5)|2(0-4](0-9)|(01)?(0-9](0-9)?)"); ’1 O.15 McnsJer von einem Ausdruck überprüft auf eine gütige IP-Adresse von 0.00.0 bis 255 255 255.255. Es^ibt hier zwar ölt den Ausdruck Xd{l,3>\.\d< L,3>\. \d<1,3>\.\d(l,3} zu vehcr\ ab^r hierbei kann dann auch 999999 999 999 «Wie gültige IP-Adresse sem (was cs natürlich nicht »$t>. '3 Der String ip wird anhand des Ausdrucks expr dd’jjfhin Ausgtwerte?, ob es eirw gültige IP-Adresse ist, und das Ergebnit wird in matüh pevpekhert ’4 Al t size ( ) wird dir Anzahl der suine^ruppen zuruck^cccbcn Im &ci$p*cl { smatch match; *2 "2 Hier ist unser smatch if(regex_match(ip. match, expr)) { *3 for(slze_t 1-0; 1 < match.size(); cout «£«":•’ « match[i) « endl; *S nt es der Wort für match [0 ] bis cout « "Länge : ” « match.lcngth(i) « endl; *6 match(4] > > cout 7 Gibt die Position der i-len Erfassungsgruppe zurück. "Position: ” « match.position(i) « endl 6 Gibt die Lange der i«teo Erfaswngv gruppe zururk iMolizl "5 . .. wobei in niatchl 0 ] immer der komplette Ertmungwtring enthalten ist (hier 192.163 1 78:1) und die einzelnen in Klammern zusammenflefossten Gruppen eben in match [ 1 ] (hier 192> bis <natch(4j (hier 1) enthalten sind Natürlich muss ich hierzu noch erwähnen, dass neben diesen Elementfunktionen noch viele weitere existieren, um das Such- ergebms zu analysieren! iBrloh-nunjI So, jetzt bist du mit den Hieroglyphen durch! Ich hoffe, du hast nicht den Kos- tümball bei deinem neuen Chef heute Abend vergessen. Deine Freundin hat sich ja passend zu deinem Pharao- Kostüm als Cleopatra verkleidet, “ d«r St«nd-»r0 653
Auch war es mehr als dringend, dass C++ mit dem C++11-Standard jetzt auch die Beschäftigung von mehren Prozessoren unter- stützt, Bisher mussten hierfür immer Third-Party-Bibliotheken verwendet werden. Wobei der Begriff Multithreading (viele Fäden) zunächst nichts anderes bedeutet als das gleichzeitige Abarbeiten mehrerer Threads innerhalb desselben Prozesses. tAChtU Bei der Verwendung von Threads musst du immer etwas umdenken! Was bei normalen Programmen eben .normal" funktioniert, kann bei Threads ganz anders reagieren, als du dies vielleicht gewohnt bist!!! Die einzelnen Threads sind Im Gegensatz zu einzelnen Pro- zessen (ein Programm bei der Ausführung) nicht voneinander geschützt. Sollten daher die einzelnen Threads die Daten gemeinsam verwenden wollen, musst du dich um eine Synchro- nisation der Threads kümmern, damit du dich in der Vieltäderei nicht selber verwickelst. (Begriff Miefrnitienl Bitte verwechsle Multitasking nicht mit Multithreading. Beim Multitasking laufen mehrere Programme unabhängig und geschützt voneinander. Wohin gegen beim Multithreading der Datenverkehr zunächst ungeschützt ausgeführt wird!!! iHinterjrundinformilronf Für echtes Multithreading ist es natürlich auch nötig, dass du einen Rechner mit mehreren Prozessoren bzw. Prozessor-Kernen hast. Nur dann ist echte Parallelität möglich. Ansonsten wird das Programm nur quasi-parallel ausgefuhrt. Um die Aufteilung der einzelnen Threads und darum, welcher Prozessor was machen darf, kümmert sich dem Betriebssystem (auch Schedu- ling genannt). Aber auch wenn du nur ein Ein-Prozessor-System vor dir hast, ist die Performance mithilfe von Multithreading wesentlich besser als für komplette Prozesse, weil hiermit ein- fach weniger Aufwand betrieben werden muss. Es lohnt sich also, sich mit dem Multithreading zu befassen. 464 SICeZCMH
Um einen Thread zu erzeugen, benötigst du die lleaderdaiel <thread> und ein Objekt vom Typ thread. Alics ist hier natürlich wieder im Namensbereich Std definiert. Der Typ thread bietet dir dann alles, was du für das Multithreading benötigst. Als Nächstes musst du dich entscheiden, wie du einen neuen Thread ausführen willst. Hierfür stehen dir die Möglichkeiten zur Verfügung, eine ganze normale F unktion zu verwenden, ein Funktionsobjekt oder eben eine Lambda-Funktion. linclude <thread> using namespace std; void eineFunkciont) < cout « "Thread als Funktionen"; > dass einFunktionsobjckt { public: void operator()() const { cout « "Thread als Funktionsobjekt\n"; > j. *1 Hier gibt« einen neuen Thread mit einer • * * Funktion.... thread tl(eineFunktion); *1 einFunktionsobjckt fObjckt; *2 thread t2(f0bJekt); *2 thread t3([){ cout « "Thread als Lambda-Funktion\n *2 .... Wer eirwn neuen Th<ead mit einem Funktianwbjekt... ’3 3 .. und hier einen neuen Thread als Lambda-Funktion. tAbhfel Wenn es möglich ist, solltest du die Lambda-Funktion bevorzu- gen. weil du dir somit unnötigen Rechenaufwand für Funktionen und Funktionsobjekt ersparen und auch gleich den Code an Ort und Stelle an bringen kannst. Wenn du jetzt einen oder mehrere Threads gestartet hast, wird direkt nach dem Aufruf mit der Arbeit begonnen. Beachte hierbei, dass auch die main ( ) -Funktion, z. B. nach dem Thread, weiter! Juft. Das zu wissen ist besonders wichtig, weil du dich hierbei auch darum kümmern musst, dass deine Threads auch ihre Arbeit zu Ende führen dürfen. Dies machst du, indem du auf das Ende jeden Threads mit join( ) wartest. £••41 • d«r ntu» St«nfl*rd 65S
tl.joinO; *1 c2.join(); *1 t3.join(); ’1 *1 Hiermit wartet die tnain (} -Funktion auf das Ende <ter ffirerfs 11. t2 und t3 lAchtunfl ist ja quasi der Hauptthread bei Multithreading- Programmen, und daher solltest du unbedingt mit join( > auf die angelegten Threads warten Ansonsten werden die Threads unsanft und unordentlich abgebrochen. [| wird tntpfohlrn. auf d4i (ndf de« Thrpjds mit join() wwjftrn Irrt wenn lÄe TNit*di beendet und, witd iwch der Hiupllfwtad beendet. Ja, es ist in der Tat möglich, du kannst das mithilfe des Gegen- stücks detach () (Verbindung lösen) machen. Im Gegensatz zu j oin ( ) blockiert der Aufrufer dann nicht mehr und kehrt sofort zurück. Damit löst du quasi die Lebenszeit des Threads vom Mainthread (oder auch Vatenhread genannt). 656 c«p;t«t SICd?tMX
iHintrrjrundinto) Wenn du, wie im Codeausschnitt gezeigt, drei Threads startest (hier tl, t2 und t3), so lässt es sich nicht vorhersehi Thread am schnellsten seinen Code ausführt und mit Arbeit fertig ist. Alle drei Threads rennen quasi wie G pferde los! Natürlich willst du auch wissen, ob ein Thread bereits ein Thread ist oder ob ein laufender Thread noch aktiv ist. Auch hierzu findest du mit get_id ( ) und j oinable () zwei einfache Elementfunktlonen. Mit get_id () bekommst du den eindeutigen Fingerabdruck des Threads zurück. Die Elementfunktion j oinable ( ) hingegen gibt true zurück, wenn der Thread aktiv ist! Ansonsten kriegst du (na was wohl) f alse zurück. Argumente für den Thread hinzuzufügen ist auch ein Kinder- spiel. Du musst lediglich die Funktion, das Furiktionsobjekt oder die Lambda-Funktion mit Parameterfn) versehen und diese(n) beim Anlegen eines Threadobjektes mit übergeben. void elneFunkciontlnc val) ( cout « "\nThread als Funktion: " « '’\nl>aten: " « val; > thread tl(eineFunkcfon, 1234); *1 •1 Ein Argument <ür den Ttireid! Es wird empfohlen, die Daten an den Thread zu kopieren, anstatt Referenzen oder Zeiger zu verwenden, um Probleme zur Lebenszeit (des Threads) zu vermeiden. Die könnten entstehen, renn eine Variable ihre Gültigkeit verliert, was wiederum ier Fall sein könnte, wenn der Thread den Aufrufer überlebt. \4rwendest du dennoch Zeiger oder Referenzen, empfehle zu prüfen. c d«r neu« 6S7
Wir nehmen jetzt die Fäden In die Hend In diesem Abschnitt sollst du jetzt auch ein wenig ein Gefühl für die Verwendung von Threads bekommen. Das Beispiel zeigt dir im Grande nur die einfachsten und grundlegenden Dinge zum Starten und Warten auf Threads, Das Beispiel Ist denkbar einfach, weil die Threads im Grunde nur etwas über S td: : COUt auf dem Bildschirm ausgeben. Statt einer stupiden Ausgabe findest du in der Praxis hier natürlich den Hardcore-Code. linclude <thread> using namespace std; val) { *1 als Funktion: M :get_id() *2 « val; ‘1 Die Funktion mrt einem Parameter wollen wir als Thread verwenden. •2 Ober this_thread (Ähnlich wie der this-Zeiger) kannst du dann Indirekt über das thread-Objekt auf dH? Elementfunktlon gct_id( ) zugreifen, um die Identifikation des Threads bei der Ausführung auszugeben void eineFunktion(int cout « "InThread « this_thread « "\nDatcn: " this_thread::sleep_for(std:tchrono::seconds(3)); *3 •3 Mit Slcep_for( ) kannst du die Auitült'ung des lautenden Tbieads für eine bestimmte Zeit Anhalten. Falls dein Compiler nichts mit sleep_for() .in/angcn kann, solltest du das Flag D_GLIBCXX_USE_ NANOSLEEP iMim Ubcr- I setzen mit angeben Coder a>$ # de fine im Code). > M Das Funkhonvobjekt mit Parameter wollen wer ebenfalls als Thread verwenden } >8 dass einFunktionsobjekt { *4 public: void operator()(const stringA s) const { cout « '*\nThread als Funktionsobjekt: « chis_chread « "\nDaten: " get_fd<) // Innerhalb von tnain() thread tl(eineFunktion, einFunktionsobjckt fObjekt; thread t2(fObjekt, "Blabla"); ‘5 thread t3([Hdouble dval){ *5 cout « "\nThread als Lambda -Funkt Ion « this_thread::get_id() « "\nDatcn: " « dval;}, 123.123 ); 1234); *5 "5 Hier werden die einzelnen Threads erzeugt und auch gleich gestartet. Bei der Version mit der Lambda-Funktion kannst du auch gleich schön Sehen, wie du einen Wert an d»c Lambda-Funktion übergeben kannst. cout « 11 \nmain() Funktion <68 sicsztMw
cl -detachO; t2.join(); r t3.join(); *' t *6 7 '6 0 cstr Thread wird vom tnain () -lhre*d ausgchjtngt und läuft fflr sich wlbst. wmit blockiert er nicht Würdest du h»cr jo£tl( ) verwenden, würde der Auftufer (hier die tnain ( ) - Funktion) w> lange blockiert. bi$ der Thread mit seiner Arbeit fertig ist. H>vraj haben wir ja zur Demonstration slecp_for( ) eingebaut. Aber teste selbst! Tausch probeweise 11. detach() gegen 11. join() aus. if(t2.jolnableO) { "8 cout « endl « t2<get_id() « " ist immer noch } eise { cout « endl « c2-get_idt) « H ist fertig”; *9 > coxit « "\nniain() Endc\n’'; aktiv"; *7 Hier warten wir auf das tndc der Threads, bevor der Aufrdet weitermacben darf *8 Ob der Thre.id noch im Rennen ist oder nicht, fragen wir hier ab. Rein logisch durfte dir wohi selbst klar sein, das^ der Thre.id nach einem joint ) nicht mchr joinab 10 ( ) IM und somit schon langst im Z»cl sein muss Daher wird hier auch ... ... diese Zeile ausgeführt O»e Ausgabe hangt jetzt davon ab, was der Standard- konstruktor dem Objekt zugrordnet hat. Das Programm bei der Ausführung: C * • 11 - d»r n«ue- Stind^rö 659
'11 Schroedinger S ./moin Thread al-. Funktion: 0x10028.1006 1 Daten : 1234 J / Thread als Funktionsobjekt: 0x1005-81000 . Daten: Blabla Thread als tambda Funktion: 0m100G0400(? Daten: 123.123 thread:: id af o non-executing threod ist fertig Schroedinger $ ,/main RQifiO Funktion Thread als Funktion: 0x100283006 Daten: Thread als Funktionsobjekt; Thread als Lcrrbda-Funkt lon: 12340xl00S810000xl00604000 Daten: Daten: 6lc*rtal23.123 thrcad::id of a non-exccutmg thread ist fertig ain(} Fndc Schroedinger S Kede br^rbriirnl Das liegt daran, dass eben alle Threads nach cout schreiben. Und so kann es passieren, dass der eine Thread eher als der andere dort hinschreibt. Wie ich zuvor schon sagte, fast wie junge Wildpferde, die einfach drauflospreschen! Natürlich kann man diese Wildpfcrdc auch im Zaum halten, so dass eine Ressource nur exklusiv vom Thread verwendet wird. Für solche Zwecke könntest du Mutexe und Locks verwenden. In unserem Beispiel könntest du etwa den kritischen Codebereich der Ausgabe über cout wie folgt schützen: Die zweite Aui^ibe verwirrt e«n weni^. *1 Alles in diesem Bcrexh steht jetzt exklusiv dem Tiuead zur Verfügung Der Nachteil dabei ist allerdings u a.r dass alle anderen Threads, die fetzt ebenfalls auf dieselbe Ressource zugzeiten wallen, geblockt werden und w.vten müssen, bis diese wieder frei ist. Natürlich musst du auch bei den anderen Threads diese Rcswurce Focken und wieder unlocken linclude <mutex> « * * Std:;mutex m; // global • • • nu lock( ) ; *1 /f Hier deine cout-Ausgabe m.unlock(); '1 {Achtung! Zwar ist die Verwendung von Mutexen relativ einfach, aber trotzdem solltest du diese nicht zu intensiv einsetzen. Wenn du einen Mut ex sehr häufig in deinem Code verwendest, kann es dir irrtümlich mal passieren, dass eine Sperre nicht mehr frei- gegeben wird. Wenn ein Thread dann auf seinen Mutex wartet und der nicht mehr freigegeben wird, kann dieser Thread bis zum Sankt-Nimmerleins-Tag warten. Zum Schutz vor kritischen Bereichen würde ich dir std: :lock_guard und std: :unlque_lock ans Herz legen, Aber dazu später noch mehr. iNotiil Natürlich kannst du bereits jetzt einen Hardcore-Code für Arbeiten mit mehreren Threads schreiben, ohne dass du eine Synchronisation brauchst. Du kannst bspw. eine komplexe Grafikfunktion für ein Bild durchführen lassen. Für solche Zwecke kannst du bspw. das Bild in mehrere Teile aufteilen (etwa vier} und dann jeweils vier Threads auf den unterschiedlichen Bereichen im Bild arbeiten lassen und am Ende in der main()-Funktion das Bild abspeichern. 660 c.ott.i siceztHX
Nur nicht den Faden verlieren Das Spinnen von vielen Fäden kann ziemlich komplex werden, und du solltest stets auf der Hut sein, keinen Faden zu verlieren. Sonst kann sich dein Programm recht schnell verheddern. lEirtlache Aufgabe] Einfache Aufgabe: Welche drei Möglichkeiten hast du, einen Thread auszuführen’ Sehr gute Antwort, Schrödinger! (fiafjchr Aufgabe] Was kannst du tun, damit ein gestarteter Thread nicht älter als sem Erzeuger wird, sondern der Erzeuger auf diesen wartet? Und wie kannst du dieses Verhältnis eines Threads zum Aufrufer lösen? 681
Hier muss Ich noch ein paar Anmerkungen machen. Beachte bitte, dass die beiden Elementfunktionen join( ) und detach ( ) die einzige Möglichkeit sind, Einfluss auf die Lebenszeit deines Threads zu nehmen. Kümmerst du dich nicht um die Lebenszeit deines Threads mittels join( ) oder detach ( ). werden diese Threads nach dem Beenden des Erzeugers mit der Funktion Std: : terminate ( ) beendet! iafjehe Ein letzte Frage noch: Wie kannst du ermitteln, ob du den Faden noch in der Hand hältst? Ich meine natürlich, ob ein Thread aktiv ist oder nicht? tzt'wn Vielleicht sollte ich noch erwähnen, falls du es benötigst, mit t2.swap(tl); kannst du zwei Threads miteinander austauschen. Des Weiteren kannst du mit t2»tl; einen Thread an einen anderen Thread nach der Move-Semantik zuwei- sen. Natürlich nur wenn tl kein aktiv laufender Thread ist! Ich sehe schon, die Threads bereiten dir keine Probleme mehr. Daher empfehle ich dir auch, dich gleich mit deren Synchronisa- tion zu befassen. Allerdings rate ich dir wie immer, davor etwas zu tun, was du besonders gerne machst. Wie wäre es mal wie- der mit einer Runde DVDs gucken? siccftHK
Im ?\bschnitt zuvor habe ich dir ja bereits kurz einen Mutex beschrieben, womit du bestimmte, vor allem kritische Codebereichc bei der Verwendung von Threads schützen kannst. Hier ein Beispiel mit einem Mutex: linclude <mutcX> '1 using namespace std; '1 Den Header brauchst du lür den Mutex. mit ex m; *2 a • dass DataRace { private; int var; public: Data_RaceO : var(O) (} int runningO ( m.lock(); *3 var++; ‘4 m.unlockO; *3 return var; >; ‘2 Hier tegil du das Mute» Objekt an. ’3 Den kritivdien Bereich hier sperren wir mit lock( ) und geben diesen wieder fre mit unlockt) *4 Innerhalb dei kritischen Bereichen wird mir die Variable Var inkrementiert. |H>nt»rgrundinioi Neben einem gewöhnlichen Mutex (std::tnutex) gibt es noch einen rekursiven Mutex (std::recursive mutex), einen mit Zeit (std:: timed_mutex) und einen rekursiven mit Zeit (std: :recursivc_timed_n*utex) Ebenso gibt es neben den einfachen Funktionen lockQ und unlock() auch noch weitere Funktionen. Beispielsweise gibt es da die Funktion try_lock(). die versucht, einen Lock zu erhalten, aber die, wenn sie keinen bekommt, nicht blockiert, oder auch ihre zeit- liche Version try_lock_until(), die ein Timeout für einen Thread setzt, um in einer bestimmten Zeit einen Lock zu erhal- ten öder es eben dann sein zu lassen. C»all - dir ntut »13
Losrennen lassen kannst du eine Horde von Threads darauf bspw. mit: Data_Race data; void race() { for(auto i-0; i<100;++i) { auto val • data.runnlngO; *1 II ... > > const int MAX - 10; thread galopp(MAX); for(auto i-0; i<MAX; i++) { *2 galopplij thread(race); *2 } '2 for(auto i-0; i<MAX; 1++) { *3 galopp[1).join(); ’3 > *3 '1 Alle Threads erhöhe« hier die vat^bieData Race: :var ’Z Wir starten auch gleich zehn Threads auf einmal,... ’3 auf die wW nat<irl«<h auch m<t join() warten müssen. Das Beispiel selbst ist recht trivial, und die Funktion sollte dich hier auch nicht interessieren. Wurdest du in diesem Beispiel kei- nen Mutex verwenden, würde je nach Zufall der eine oder ande- re Thread die Daten von Data_race; :var ändern. Das ist ein Zustand, der eine Art .kritischen Wettlauf' darsldlt und den man eben so wohl nicht haben will. Wilde Threads, die auf Daten operieren, können zu schwer auffindbaren Programmier' fchlcm führen. IHtatrrgrundinfal Wenn sich mindestens zwei Threads um dieselben Ressourcen streiten, wird das häufig auch als Race Condition (oder Data Race) bezeichnet. <64 c.c.;t«i siceztMw
• der ess
Aber wir haben hier ja einen Mutex verwendet. Trotzdem nicht gut genug? Im Grunde schon, aber dieser Mutex ist nur eine einfache Form und kann doch böse Fehler verursachen. Sghoit wir uns hierzu einfach nochmals diesen Cocfemisschnitt an: *1 Ein Problem ivt hle^ d.i« tich hinirr unlockO noch c^nc* Anwcitunfl beendet 0« konnte bedeuten, dw hier ein anderer Thread bereits wieder den Wert von var um 1 erhöht hat und hier dann eben schon wieder ein weiterer erhöhter Wert Jurikkgegeben wird. Int runnirigx) < tn. lock(); var++; m.unlock()» return var; *1 Das Problem lässt sich zwar beheben, indem du einfach den Mutex mit lock( ) und unlock( ) an Ort und Stelle der Funktion race ( ) elnsetzt. Aber die Erfahrung lehrt uns auch hier, dass der Programmierer oft in einem Mullithread-Programm mehrere lock( ) und unlock ( ) Anweisungen verwendet und vielleicht auch mal das eine unlock( ) vergisst, Das Prinzip Ist Ja schon vom Reservieren von Speicher bekannt, bei dem man auch gerne mal nach einem new seinen Schmutz nicht mit delete aufräumt. liier haben die O*-Entwickler Ja mit den Smart-Pointern nachgelegt! (Achtung! Neben dem versehentlichen Vergessen, einen Mutex wieder freizugeben, kann es auch passieren, dass wahrend der Aus- führung des kritischen Codebereiches eine Ausnahme geworfen wird und somit der Mutex ebenfalls nicht mehr freigegeben wird, weil der Thread nicht mehr unlockf )aufrufen kann. Für eine sichere Verwendung und um vorangegangene Probleme mit einem Mutex bei den Theads zu vermeiden, werden diese einfach in sogenannte Locks gepackt. Für solche Zwecke findest du Jetzt die Klassen lock_guard oder unique_lock vor. «86 SICeZCMH
Ihre Verwendung ist ziemlich simpel mutex m; dass DataRace ( private: int var; public: Data_Race() : var(O) {} int runningO { lock_guard<nmtex> waechter(m); *1 ’1 De- lock guard ruft hier Intern m. lock( ) .wf und sperrt diesen Codcj^c*di. var++; return var; } ’2 ’2 Der Destruktor von lock_guard wird hingegen r-rst .im'tnde OC5 Anweiuins;^. blocks prüfen. vertilgt weh hinter diesen Locfcs nichts anderes als eine Sperre, die über den An>veisungsblock gültig ist (scoped locking) |AdhlUH£| Somit gilt also folgende Regel für dich: Lass die Finger von der direkten Verwendung eines Mutexes und wende stattdessen die Verpackung lockguard bzw. unique_lock darauf an. Bei den vielen Techniken, die Muhithrcading anbietet, solltest du nicht die einfache- ren Dinge im Leben übersehen. Wenn du einfach nur Variable geschützt initialisieren willst, stehen dir drei einfache Möglichkeiten zur Verfügung: den Konstruktor als konstanten Ausdruck mit constexpr zu definieren 2. statische Variable innerhalb eines Anweisungsblocks zu verwenden das neue Funktions-Template Std : :call_once zu verwenden |ZeU«n Falls du nur thread-lokale Variable benötigst, kannst du dir noch mehr Aufwand sparen, weil dir hierzu das Schlüsselwort thrcad_local zur Verfügung steht, welches du nur vor deine Variable setzen musst. Im Grunde handelt es hierbei nur um eine statische Variable, die initialisiert wird, sobald du sie zum ersten Mal verwendest, und die sich im leben des Threads nicht mehr ändert. C**X1 * der ntut 667
’3 ... blocken beide Threads mit dem ersten Lock und warten beeide auf die Freigabe (Oeadkxk}. Im ßeupiel kannte i Bte Intimi; tehr oft ahne weil die Laufzeit def Thr *gunstig" wir. abef dann wa irgendwann Schluss. Ou kannst hierzu auch ein wenig künstlich mit Totgesperrt Tja.jetzt hast du vielleicht den kritischen Wettlauf (Race Condi tion) deiner Threads um eine Ressource gezähmt. aber du kannst dein Programm jetzt immer noch .totsperren- (hier Ist natürlich die Rede von einem Deadlock). Ein solcher Deadlock kann pas sieren, wenn mindestens zwei Threads auf die Freigabe einer Ressource des anderen Threads warten. Genau, richtig mensch- lich! Wir warten auch immer, dass der andere schon den ersten Schritt machen wird! Das Problem lässt sich ganz einfach be- heben, indem du die Locks atomar lockst! Atomare Operationen werden in einem Rutsch ohne Unterbre- chung ausgefuhrt und sind nicht „teilbar Das heißt dann auch, diese werden entweder ganz ausgeführt oder gar nicht! Hierzu eine einfache Oenonstration, wie wir uns selbst ins Knie schießen können: dass Dead private: int var; public: mutex ms Dead() ; •1 var(0) <} 568 szcezcMH *1 Hier ist er, unvrr Mutcx' *2 In de* Funktion Dead_ Lock( ) werden zwei Ressourcen getackt. Auf den ersten Blick nicht schlimm! Aber wenn die Laufzeit .ungünstig-* ist,... void Dead_Lock(Dead& dl, Deadi d2) { lock_guard<mutex> waechterOl(dl.d); *2 cout « "Mutex No. 1 aktiven"; //this_thread::sleep for(chrono::millisecon ds(1000>); *4 lock_guard<routcx> wacchter02(d2.m); *2 cout « "Mutex No. 2 aktiv\n"; Dead totOl» thrcad t01( d t02( ItOlJolr.f); t02< jcdnO । ad_Lock(tot01> tot02);}); *3 ad Lock(tot02, cotOl);}); -3
No. 1 aktiv No. 2 aktiv No. 1 aktiv No. 2 aktiv Schroedinger S ./main Mutex Mutex Mutex Mutex Schroedinger S ./main Mutex No. 1 aktiv Mutex No. 1 aktiv Bttte TMeidi und St Utopie urtd den ersten lock n*cM mehr frei. »ondrm widfn jrl-rt b*i ium Sinkt*-N«mm«rleins*TAg oder eben Im der Anwender dl» Prof nimm rwl Crwilt beendet Das Problem können wir jetzt ganz elegant mit unique_lock wie folgt unter den Teppich kehren: 4 .. eieer Pause fiachhelten, um gleich ein Deadlock tu verurwehen, mit dem du licbergelnt. d.i« beide Di/eidi kurz ichlifcn und sonnt den zweiten L<xk gar n»ctit mehr bekommen. void Dead_Lock(Dead& dl, Deadi d2) { unique_lock<[Dutex> waachterOl (d 1 .n, defer_lock) ; *2 cout « "Mutex No. 1 aktiven"; Ifthis_thread::sleep_for(chrono::ailliseconds(1000)); unique_lock<mutex> waechter02(d2,ci, defer_lock); *2 cout « "Mutex No. 2 aktiv\n"s lock(waechter01. waechter02); *1 •1 Dai ist die entscheidende Zeile in dieser Funkhon Mit Std: tlock() birwfen wir du* Locks quas» atomar Das bedeutet jetzt, dass der autrufende Thread entweder beide Locks erhält oder keinen' •2 Eine wiche Verwendung «Pit std:: lock( ) benötigt einen verzögerten Mutex, den du hier mitunique lock irnd der Opt'Wl std: tdefer lock mietest. jNotitl An dieser Stelle muss ich natürlich noch hinzufugen, dass unique lock weitaus mächtiger ist als lock guard Die Klasse unique_lock bietet noch viele weitere Element- funktionen. wie das versuchsweise Locken, das versuchsweise Locken auf Zeit, das Tauschen von Locks, die Freigabe von Locks.die Rückgabe von Locks usw. - d»F ntut Stmdirö 659
Einmal bitte... Willst du eine Funktion genau nur einmal aufrufen und rnlt einem Wert initialisieren, kannst du dies mit std s : call_once und std: : once_f lag realisieren. Das ist ganz besonders nützlich» wenn du ein Objekt dynamisch angelegt hast. Neben dieser einmaligen Sache will ich dir in dem folgenden Listing auch den constexpt Kontruktor und die Sache mit der statischen Variablen im Block demonstrieren. mutex m; dass Huhu { int var; public: constexpr Huhuf):var(0) {} *1 Huhu (int i__) : var(i_) {} int getVaK) const { return var; } void setVal(int 1) {var-i;} *1 Hier findet eine italische Initialisierung mithilfe eines constexpr Konstruktors SUK •? Hier fordern wir dynamisch einen Speicher zur Laufzeit an. Huhu* huhuPtr = nullptr; std::oncc_f lag flag; void mylnstance(int val} { huhuPtr new Huhu(val); *2 > void initOncetinc val) { lock__guard<mutex> waechter(ni); *3 std::call_oncc(flag, mylnstance, val); *4 //mylnstance(val); cout « this_thread::get_id() « " : cout « huhuPtr->getVal() « endl; II Hier weitere Arbeit } void statVar(int val) { lock_guard<mutcx> waechter(m); *3 static Huhu hu(val); *5 cout « this_thread::get_id() « " : cout « hu.gecVaK) « endl; *3 Damit syfKhromderen wir nur die Ausgabe von der Ressource COut. damit es nicht zu eine*n Durcheinander kommt und ordent- lich ausgegeben wird. 4 Das Tcmpl.rte call_Once benötigt das Flag CÄ1 l_f lag und cme aufrufbare Emhcrt [hier die Funktion mylnstance) und die nötigen Argumente. Hierbei kannst du übrigens beliebig viele Argumente verwenden. Diese Funktion mylnstance wird taiiathhth nur einmalig aufgerufen. "5 Crhcblich einfacher als m<i std: : call_oncc karwisi du Gleichwertiges mit einer statischen Variablen in einem Stockt et eich erzeugen Auch ist absolut Th/ead »sicher. <70 Udt«! SICSZtMk
II Hier weitere Arbeit Huhu huhuOl; *1 thread tKinicOnce, 12345); *6 thread tZ(initOnce, 45678); *6 thisthread: :sleep_for(chrono: :cnilliseconds( 10)); initOnce(98765); ’6 cout « this_thrcad::get_id() « " : cout « huhuPcr->getVal() « endl; thread t3(statVars 54321); *7 this_thread:: sleep_for(ehrono: :Eailliseconds( 10)); thread t4(statVarf 66666); *7 tl.joinO; t2,join(); t3-join(); t4.join(); ‘5 Der Thread 11 ruh initiOnceO mildem Werl 12345 auf. initOncel ) wiederum ruft über std::call_once() die Funktion my I n s tance () auf uM übergibt dem Objekt den Wert 12345. Der /weile Thread t2 will dawelbe machen, aber et wird hierbei nicht mehr std:: call_once () auf- gerufen, d.« gehl nur einmal. Somit mutfcfich 11 jetzt auch mit dem Wert 12345 begnügen und k.inn kein neue* Objekt mit dem Wert 45678 erzeugen Da bringt auch kein Aufruf von intiOnce() in dei rnainf )- Funktion mit dem Wert 98765 was. Alle drei Threadi muwen mit 12345 alberten. •7 Owelbe gdt auch hier beim Thread t3 mit der Funktion StatVar( ). womit e n st.itiKhe^ Objekt Huhu mrt dem Wert 54321 im Stock der Funktion erzeugt wird, Somit muss auch der Thread t4 mit 54321 arbeiten. - dor «tut StanUaro 671
Am Ende des Fodens Normalerweise endet unser Tag hier ja immer mit ein wenig Entspannung auf dem Sofa oder so. Aber da dies hier unser letzter Abschnitt zu den Multithreads ist. will ich noch die Gelegenheit nutzen, ein paar Dinge einzuschieben, die noch nicht behandelt wurden. „Kondltions“-Variable...? Manchmal ist es auch nötig, einzelne Threads nach Ereignissen zu steuern. Für solche Zwecke bietet diese Bibliothek: Bedingungsvariable (std: : condition_variable} aus der Headerdatei <condi- tlon_vatiable> an. Damit ist eine einfache Synchronisation der Threads über Ereignisse möglich. Das Grundprinzip ist einfach. Du schreibst einen Thread, der als Empfänger auf irgendetwas wartet, um mit seiner Arbeit forizufahren, und einen anderen Thread, den Sender, der vielleicht auch irgendetwas zu tun hat und dem Empfänger sein Signal gibt, dass er loslegen kann. Folgender Codeausschnitt soll dir den Sachverhalt vereinfacht demonstrieren: linclude <thread> linclude <condltion_variable> linclude <im.it cx> •1 Oie Bcchngungsvariable wird hier global verwendet, weil diese sowohl dem EmpJinger ah auch dem Sender zur Verfügung stehen muss. Das Glei- che gilt hier auch für den Mutex und den Wahrheitswert, mit dem wir Anzeigen wollen, dass Oalcn vorhanden sind. using nassespace stdt mutex a; *1 condicion_variable cond; ’1 bool go; *1 *2 De» Emplinge» lockt «machst den Mutex. void enpfaengerWartetAufArbeit() { uniquc_lock<mutex> lock(m); *2 cout « "Empfänger: Ich warte . cond.walt(lock. [) (return go;}); *3 cout « "Empfänger: Ich habe was zu tun } void senderBereltetArbeitVor() { lock_guard<mutex> lock(a); *4 •4 Auch der Sender lockt /unJch^t den Mutex <72 «fepltel SletztHX
cout « "Sender: Bereite Arbeit vor . thisthread::sleep_for(chrono::»illiseconds(2000)); *5 go=true; *6 *5 Kemer arbeitet hier wirklich! Unsre Vorarbeiten bestehen aus 2XXX> AVlhsckumkn warten ;) . cout « "Sender: Los geht'« ...!\n"; cond.notify_one(); *7 thread tOl(empfaengerWartetAufArbeit); thread t02(senderBereitetArbeitVor); cOl*join(); t02.join(); ‘6 Sind wir mit der Arbeit fertig, M?Uen wir zunächst den Wahrhcits- wert auf true und ... *7 . . uhkken dann der damit verbundenen Bedingun^svariablen cond mit der Elemerefunklran notify_one( > den Wink. dm der Thread aufwachen soll. Belm Sender müssen wir nur einmal etwas locken. Belm Emp- tanger hingegen müssen wir für gewöhnlich öfter den Lock ein- setzen und wieder freigeben. Und das geht eben nur mit dem mächtigeren Ixnck unique_lockt Und. ja. du könntest theo- rctisch auch beim Sender den mächtigen Lock verwenden. (Notiz] * Neben notify_onc(). um einen wartenden Thread mit einer Bedingungsvariablen aufzuwecken, gibt es u.a. auch noch notify_all( >. um alle wartenden Threads aufzuwecken. Natürlich sind noch weitere Elementfunktionen dazu vorhanden. "3 Mit wait ( ) wiwenden wir hier gleich die Bedingungwariable cond Ah Argument erh.t’l Wait ( ) der» Lock und ein PrJldik.it (hier eine Lambda- FunkiHHi). wekhes d.ifur sorgt. dass def Thread wieder aufwachen kann. Im Beispiel •st dies gegeben, wenn der Boolesche Wed go den Wert true erhalt (Zelten Auch asynchrone Funktionsaufrufe kannst du mit std:: async (> tätigen. Damit kannst du quasi ganz ohne Locks oder Bedingungsvariable einen extra Thread starten und das Ergebnis spater abholen. Der Job. der einen Wert erzeugt, wird als Promise und der andere Job. der ihn irgendwann abholt, wird als Future bezeichnet. C*-ll - der <n«ue Stenderö 673
Zusammenfassung Mit dem Thema Threads könnten wir noch ganze Selten lüllcn. aber die Grundlagen dazu kennst du jetzt. Du weißt, wie du Threads erzeugen und verwalten kannst. Du weißt auch, wie du gemeinsam genutzte Daten mit Threads verwenden und schützen kannst. Auch das Synchronisieren von Threads mit Bedingungsvariablen ist dir jetzt nicht mehr ganz unbekannt. Gratuliere dir. Schrödinger! Du bist durch mit der C+t-Einführung. Du kannst deinen Chef anrufen und ihm sagen, dass du bereit bist, deinen Job anzutreten Bei all deiner Euphorie solltest du aber immer sehr wissbegierig und lernwillig bleiben, weil es ja eigentlich jetzt erst richtig losgeht. Die echte Praxis und der (manches Mal) graue Programmieralltag stehen dir erst noch bevor. Aber du bringst viele Grundlagen mit. Ich wünsche dir viel Erfolg bei deinen Projekten! 474 siceztMK

p
Inra
Symbole -> 295 . 295 \\ 196,243.287 & 203 && 611 * 255 « 54 überladen 352. 356 » 54 überladen 352, 356 ~ 306 11 73 <array> 619 \b 73 <condi(ion..variablc> <CStdlib> 45 DATE_ 264 \ddd 73 •define 256. 261 •elif 258 •eise 258 •endif 258. 259 (ENTER) 161 •error 264 If 73 _FILE_ 264 •If 258 •ifdef 258 •ifhdef 258. 259 •include 256, 259,267 <limits> 82 •line 264 __LINE_ 264 <mcrnory> 628 <tnutex> 1.60. 66 5 \n 73 •pragma 264 <regex> 643 «Strings 47b cthread> > 55 _T1ME_ 264 «78 überladen 346 !, überladen 349 !, überladen 351. 354 II. überladen 61 +. überladen 338 überladen 346 +=, überladen 340 «..überladen 351,354 •undef 257 <unordercd_map> 620 <unordered_set> 620 \uXXXX 73 \v 73 \xhh 73 A Abbruchbedingung 1 Abgeleitete Klassen 364 Ableitung 371 abort 223 abonO 471 absO 99 Abstrakte Klasse 408 Adapterklasse 549 Addition (+) Sb adjaeent.ftndO 593 Adressoperator 177 182 advanccO 588 Akustisches Signal Algorithmus 551.581.S92 £+♦11 621 für sortierte Bereiche 59? löschender 594 modifizierender 593 mutierender 595 nicht modifizierender >92 sortierender 596 Aliase Templates 440 alLofO 621 AND siche: Logisches UND Anführungszeichen 76 Anwcisttngsblock 41 195
anyO 577 any.ofO 621 argO 99 Argument 106 Arithmetische Berechnungen 101 Arithmetischen Operationen iox Arithmetische Operatoren Si. Array 619 ASCII-Code 70 ASCII-Tabelle 74 ASClI-Zekhensatz ?6 assignO 561 asyncO 673 atO 560 Ausgabe formatieren 4V Ausgabe, unformatierte 498 Ausnahmcbehandlung 441 Ausnahmefehler 223 Ausnahme-Handler -4 t auto 232. 239.607, 613 auto_ptr 628 B backO 560 bad() 523 bad.alloc 463 bad cast 463 bad.exception 463 bad.typeid 463 basic. 496 Basisklasse 365 Basistypen 58 Bedingte Kompilierung 258 263 Bedingungsvariable 72 Behälter 5411 assoziativer 570 sequenzieller 558 Bezeichner 59 eindeutig, gültig 66 Bidirektionaler Iterator 5X4 Binär 90 Binärcode 40 ?c Binäre Operatoren überladen 3 32 binary.scarchO 597 Bits 79 bitset 5511 572 bool 79.83 boolalpha 514 Boolescher Ausdruck 132 Boolescher Typ 63 boost-Blbliothek 1 i break 126.128 Buffer-Overflow 145. 155: siehe auch: l’ufferüberlauf Byte 79 c Call by Reference 202 Call-by-Vahte ni call_oncc 670 capacityO 561 C-Array 619 case 126,128 Case sensitivlty 42 Cast-Operator überladen 351, 356 catch 444 catcht...) 444 cerr 50. 51 Char 70. 71. 75. 76. 79. 81. 83.125 char 175 char16_t 73,84 char32_t 73. 84 ein 51. 52. 55 dass 163.279.421,429 deirO 523. 561.571 clog 50.51 closeO 530 Codepage 850 45 condition.variabte 672 conJO 99 const 208. 232, 240. 261. 319 const_cast<TYI’> 109 consicxpr 610. 670 Container siehe: Behälter copyO 594 Ifidev 67®
copy.backwardO 594 copy.nO 621 COUntO 571,593 count ifO 59? cout 50. 51.53. 55. 75 c_strO 537 D Data Race 664 Datei 526 lesen 531 öffnen 527 Position 527. 536 Puffer 527 schließen 530 schreiben 531 Datenkapsel 27« Daten, Klassen 28 t, 319 Daten speichern 58 Datentyp, abstrakter 278 Deadlock 668 dec 501.514 decltype 607,613 default 128.609 defaull-Marke 127 Default-Farameter 220 Definition 60 Deklaration 60,66,268 Dekrement 134 Dekrementoperator 134 delete 185.609 überladen 361 deque 549, 558 Destruktor 306 307,318 Vererbung 389 virtual 413 detachO 656 Dezimale Schreibweise 62 Diakritische Zeichen Direktive 255 distanceO 588 divldes«T> 5R3 domain_error -161,466 Doppelte Genauigkeit '• Dos-Latin. 1 45 double 67 84. 98. 108. 184 do-while-Schleifc 131 dynamlc_cast 463 dynamic_cast<TYP> 109 dZeiger 185 E Einfache Genauigkeit ’s Einlesen, unformatiert 505 Einzelne Anführungszeichen 71 Element funktion 153. 155.281,286 const 288 Klassen-Template 430 Spezialisierung 131 überschreiben 431 Elementinitiali derer 192 cmptyO 561 endl 52. 53. 515 ends 515 eofO 523 EOF 525 cqualO 593 eqtial_range<) 597 equal_to<T> 583 eraseO 561, 571 Erweiterte Genauigkeit ‘8 exceptio n 461 Exception-Händler 444 excepiion handllng 442 exit 223 expliCil 314.316 explizit 233 Explizite Typiint Wandlung Extern 230.234.238 failO 523 Fehlerklasse 453 Fehlermeldung 47 «80
Fehlerüberprüfung i« fillO 499. 594 filLnO 594 final 372 fmdO 571.592 find_endO 592 find_lirst_ofO 593 findJfQ 592 find lf_notO 621 fixed 501.514 H-agsO 500 flipO 572 float 84,98 Floating polm 67 flush 515 for_eachO 582.592 for. Range-based : for-Schleife 131 Forward-Iterator 584 forward list 620 friend 322.330 Operatoren überladen i M frontO 560 fstream 527 Fundamentale Typen 66 Funktion 106. 194 beenden 223 Definition f Deklaration 197 Flementi'unktion ! •'4 inline 222 mit Parameter 201 Rückgabewert 211 Standard paramctcr 220 Funktionsaufruf 198 Funklionsobjekt < ’ 581 vordefiniertes 582 Fun kt ionsopcrator, überladen '• Funklionsparameter als C-Array 204 als C-String 204 als Kopie 201 als Referenz 203. 206 als Zeiger 202 const 208 Klassen 205 Strukturen 205 Funktion? rückgabewert 211 als Referenz 214 als Zeiger 212 lokale Daten (!) 212 zwei Werte 218 Funktions-Template 420. 543 Funklionsüberladung 221,225 227 Funktor 517. 58 t G Ganzzahlen 62. 65. 76. 79.94.134 Ganzzahltypcn 59.61,83 generateO 594. 599 generate. nO 594 getO 505 get_idO 657 gctlineO 489. 506 geicN>(bezeichner) 622 Gleitkommatypen 59. 67. 68. 84 Gleitkommazahlen ' 125 Gleitpunkttypen < goodO 523 greater_equal<T> 583 greatercT* 583 Groß-und Kleinschreibung Grundlegende Typen Gnindrechen Zuweisungsoperator s ; Gültige Werte 61 Gültigkeitsbereich 195.199. 238. 245 Destruktor 108 H Has lila bei le 620 Headerdatel 43. 99. 266. 268 <iostrcam> 48 hex 501, 514 Hexadezimale Schreibweise
Hexadezimale Werte 45 Hexadezimale Zahl Hilfszeiger 190 I If 82. 125 if-An Weisung If-Anweisungsblock :v ifslrcam 527 igrwreO 161. 509 II.P32 84 II.P64 84 Implementierung 68 Implizite Konvertierung 110 Indexopcratorl | 143 Indexoperator, überladen 361 Indircktionsoperator 1 ’ü Initialisieren 61, 67 Initialisierung der Schleifenvariablen l . Initialisicrungslistc 305, 309. 319 Inkrement 134 Inkrcmcntoperator 134 inline 222. 262. 287. 425. 430 Input-Iterator 583 insertO 559,571 Insert-Iterator 585 Instanz 279, 294. 299 int 61.66. 81,83, 108, 125 Integer 61 Integrale Ausweitung 111 internal 501. 514 lnvalid_argumeni 461 465 iomanip 515 los:: ios::app 529 ioszate 529 lo$::badbh 52? ios::beg 536 ios::binary 529 los::cur 536 ios::end 536 ios-eofbit 523 ios::fallbit 523 io$::lmlHag$ 500 ios::goodblt 523 ios::in 529 ios::openmode 529 ios::out 529 ios::trunc 529 ios_base 496 ios_basc::iailurc 462.4b ' iostate 522 iostream 43 iostream Bibliothek 4-H, 49 iotaQ 621 ISO-Latin-1 72 is.openO 530 IST-Beziehung 365 istream 905 lstream::getllneO 511 istrlngstream 541 Iterationen 131 Iterator 551 /kdapter 585 Kategorien 583 itcr_$wapO 588. 594 J joinO 655 JolnableO 657 K Klasse 279 abstrakte 408 Ausnahmen 453 Daten 280 dynamische 320 konstante ho statische 321 Hehler 453 Objekte 299 Zugriffs rechte 3'3 «82
Klassendefiniilon 280. 281. 284 Klassen-Template 429 Elementfunktionen 430 Objekte erzeugen 4 ?2 Klassische Konvertierung 11 Kleiner 120 Komma 60 Kommazahlen 134 Komplexe Zahl 101 Konstruktor 302 mit Parameter 304 Vererbung 389 Konvertierungskonstruklor 314, 31(> 31 r Kopie, flache 124 Kopierkonstruktor 312.316,317 318,320 324,325 Kopie, tiefe 325 Korrekte Konvertierung in Lambda-Funktion 59'., >: :)8 Laufzeit 183 Leerer Vektor 159 left 501.514 length_error 461.466 less_equal<T> 583 kss<T> 583 IlSt 549. 558 LLP64 84 lockO 669 Lock 666 lock_guard 666 logical_and<T> 183 logicai_not<T> 583 logieal_or<T> 583 legte error 4 t. 1 Logischer Operator 122 Logisches NICHT 122 Logisches ODER 122 Logisches UND 122 long 61.83. 125 long* 175 long double 84.‘in long (int) 6<> long int 61. 63 long long 84 lower_bound0 M Mac 44 malnO 41.. 42, 22 1 MainO 46 inain-Funktion 159 make heapO 59b makeJupleO 622 Makro 256,261 Manipulator 52, 513 benutzerdefinierter 516 mit Parameter 517 ohne Parameter 516 map 550. 570 Maschinencode 40 maxO 81 maX-SizeO 561 Mehrfachvererbung 3:,6 virtual 403 Memory Leaks 217 siehe auch: Speicherlecks ntergcO 562. 597 minO 81 min eiementO 59 3 minuscT» 583 mismatchO Modul 265 modtilescT* 583 Modulo-Operator moveO 562,612.621 move backwardO 621 Move-Semantik 611.615 MS Windows 44 inultimap 550, 570 inultiplies<T> 33 multlset 550. 570 inutex 660, 6453 Index »83
N Namensbereich 2-12 NamcnskonHikt 243 Namensraum 241. 246. 252 anonym 250 std 43 namespace 241.242 negate<T> 583 new 463 überladen 361 next pcrmulationO 695 noboolalpha 514 noexcept 4 " none_ofO 621 normO 99 noshowbasc 514 noshowpoini 514 noshowpos 514 noskipws 514 No such file or directoty 27t NOT siche: Logisches NICHT not_equal_to<T> 583 notlfy.oneO 673 nounitbuf 514 nouppercase 514 nt.elemeniO 596 nullptr 60 609 0 Objekt 278.293.296 const 319 erzeugen 294 Klassen-Template 432 Objektzeiger, überladen 359 OCt 501,514 ofstream 527 Oktale Schreibweise 62 Oktale Zahl 73 once Hag i,:’O OOP 278 openO 528 operator 332 opcralorO 517 Operator 80. 86. 90.120 gleich 120 größer 120 größer oder gleich *20 kleiner 120 kleiner oder gleich 120 nicht Qberladbar >• < < überladbar 333 überladen 332 friend-Eunktion 3 •*> Vererbung 3'5 ungleich 120 Operatoren 54 OR siehe: Logisches ODER ostrcani 497 ostringsircam 41 out_of_range 461,464 479 Output-Iterator 584 ovcrflow_error 462 P palro 572 parttal,$onO 596 partial_sort_copyO 596 partitionO 595 Pfeiloperator. Klassen 295 plu$<T> 583 polart) .19 Polymorphie 411.416 popO 561 pop backO 561 pop.frontO 61 pop heapO 59t Post fix Schrei b we ise 13 4 Prädikat 583.590 Präfix-Schreibweise 1 •: l Präprozessor 43. 255 predsionO 49 ' prcv_pcrmutationO 595 Priorität der Klammerung 95 684
priority_qtteue 550. 560 private 282. 285. 373. 377 Mitglieder durchreichen 481 Programmdatei. ausführbar Programm ende 223 protec ted 373. 379 Prozessorarchitektur 84 public 282. 285. 373, 378 Puffer so Puffcrüberlauf 14'.149 Pufferunierlauf 147 Punkteoperator 158 Klassen 295 Punkt vor-Strichrechnung 87 pushO 560 push_backO 559 pushJrontO 559 push_hcapO '96 putO 506 putbackO 506 Q Quelldatei 266. 269 queue 550. 560 R Race Condition t 4 randO 599 Ra ndom-Access-Iterator 584 random.shuffleO 595 Range-based-loop 608 range_error 462 Raw-String-Literale 480 rbeglnO 585 rdstatcO 523 recursive_mutex 663 recursive timcd mutex > Redeflnilicm 583 Referenz 203 zyklische 631 Regel der Großen Drei 318 regex 643 regex_error 462 rcgex_match 643, 645 regex.replace 643 ,.4 regex search 643. 646 Regulärer Ausdruck < • 3 9 reinterpret_ca$i<TYP> 10' Rekursion 386 removcO 56?. 595 remove.copyO 595 remove copy.ifO 595 removeJfO 595 rendO 585 rcplaceO 594 replace_copyO 594 replace,copy,if’0 594 replaeeJfO 594 reserveO 561 resetO 572 resetiosflagsO 515 resizcü 561 return 106, 211,223 reverseO 562. 567. 595 reverse copyO 595 Reverse-Iterator 585 RGB 170 right 501. 514 rotateO 595 rotate.copyO 595 Rückgabetyp 106 Rückschritt/Backspace 73 Rundungsfehlcr 4 runtiinc_error 461 Rvaiue-Referenz 11 s Schleifen 131 Abbruchbedingung 132 Variable 135 scientific 501, 514 searchQ 593 search_n() 593 seekgO 536 885
seekpO 536 Seiten Vorschub Semikolon 60.133 set 550.570 setQ 572 setbaseO 515 set dirtcrenceO 59? setfillO 515 setJniersecikonO 597 setiosflagsO ?15 setprecisionO 515 setstateD 523 set_symetric_diiYcrnccO 59 secicrminateO 471 set unionO 5 97 setwO 515 shareci.ptr 628. 6 io short 61, 63 short (int) 66 showbase 501,514 showpoinl 501, *514 showpos 501.514 signed 66.71.113 signed char 83 slzeO 151.561 sizeof 80, 81, 175 skipws 514 Smart Poinicr < .ts smatch 644 SOrtO 562.596 sort.hcapO 596 Spagetti-Code 136 Speicherklasse 2 30 Speicherlecks 148. 192 Speichern von Daten 58 Speicherobjekt Speicherverbrauch in Bytes 81 Speicherverwaltung 183 Spezialisierung. Klasscn-Tcmplatc 431 splieeO 562 sregex.itcrator 648 sregex.token.iterator 651 sstream 541 «es stable.panlrionO 595 stablc.sortO 596 stack 550, 560 Stack-Unwinding 45 ; StandardfchlerkJasscn 4> 1 Standardkonstruktor 302 325 Standardkopicrkonstniktor t1 Standardpa raineter 220, 224. 227 Stand»rdsirom 497 static 231, 236. 23«, 321 static_cast<TYP> 108 std 55 std" 244,249 SldiO 501 Sternchen 1 -4, 182 STL 429,476.548 Stream 48: siehe auch: Strom Strcam-hcrator 585 String 144 150.154.476 + 48« +- 488 « 488 == 4«8 » 488 appendO 485 atO 479 capacityO 480 clearO 480 copyO 485 C-Strlng 485 dataO 485 einptyO 480 eraseO 485 findO 486 find.fi rst.ofO 486 gctlineO 489 Indexoperator 479 insenO 485 Länge ermitteln 480 lengtltO 480 max.sizeö 480 npos 486 Operatoren 488
replaceQ -«85 rcsizcü 480 rfindO 486 slzeO 4BÖ stringslream 541 Strom 541 Suche 486 verkeilen 488 Zuweisungsoperator 47? string::gctlincO 511 Strom 496 Ausgabe formatierte 499 unformatierte 49« Dateien 526 Eingabe 505 Manipulator 51 : Standard- 497 String 541 Srrousirup, Bjarne -I >• struct 163. 164 Strukturzelger 187 Suchen,string 486 ssvapO 562 swap rangesO 594 switch 125.126. Symbolische Konstante 76 systcmO 45 system_error 462 T Tabulator, horizontal •’ ? Tabulator, vertikal 3 tellgO 536 tellpO 536 template 421 429 templateo 425 Template, Funktionen 420 Template, Klassen 429 Temporäre Struktur b .) tcrminateO 444 Terminierungszeichen 53 «his-Zeiger 297. 300 thread 655 threadjocal 667 throw 444, 448 timed_mutex । toJongO 572 lopO 560 lO-Sl ringt) 572 transformO 594. 5") «nie siche: Wahr ity 447 tuple 622 Turmbau zu Babel 2 typedef 172.434 «ypeid 415,427,463 lypeinfo 415.42? Typen Basislypcn 58 komplexere 66 lypename 421,429 Typnamen vereinfachen ' lypquallfikatlon 23? const 232 TypumWandlung, Klassen '1 u Überladen Operatoren 332 binare 332 unärc 344 Überlaufende r Wert 110 Ubuntu-Llnux 44 Unär 90 Unsre Operatoren überladen 344 Undclincd Symbols 271 underflow_error 462 ungetO 506 Unicode 73, 74 union 164.172 unlqueO 595 untque_copyO 595 uniquejock 666. 669 Ind*» 687
unique_ptr 628.633 unitbuf 501. 514 unordered_map 620 unordered muh i map 62C unordered_muhiset b2ü unordered.set 620 unsctfO 500 unsigncd 62. 63, ‘1. 113 unsigncd char 83 unsigncd int 83, los. 143 unsigncd long 83 unsigncd long long 84 unsigncd short 83 Unterst rieh reichen 59 unwahr 63 upper.boundO 59? uppercase 50i.514 using 241.244 using namespace std 249 U5-Schreibweise UTF-8-Kodicnmg 72. 73 UTF 8 Uterale 45 V vector 150, 151. 154, 299. 549, 558 Vererbung 364. 371 Destnikioren 389 Elcmentinitialisierer l|2 erweitern 382 Konstruktoren 389 Mchrfachvcrcrbung 396 Operatoroberladung Redetinilion 383 Zugriff auf Mitglieder 38 3 Vergleichsoperatoren 120 Vielgestaltigkeit 411 Virtual 23 3. 404. 408 Destruktor 413 Virtuelle Vererbung 403 void 60 volatile 233 Vorzeichen 65. 66. 82 vorzcichenlos 62 w Wahr 63 waitO 673 weerr 76 wchar.l 75.79,83 wein ’b WCOUI 75, 76 wcak_ptr 628, 631 Weihwasser 60 Weisheit Werte, negativ 62 Werte, positiv 62 whatO 462 while*Schleifc 131 widlhO 499 Wikipedia : 8 WoW 58 ws 515 z Zcichenkctte 144 Zeichenliteralc Zeichentypcn 7ü Zeiger 178: siehe auch: Sternchen Zellenende 73 Ziel bereich des Puffers Zufallszahlen 23? Zugriffsoperator 196. 24>. 248. 287 Zugriffs rechte 373 Zugriff, wahlfreier 53i> Zuweisung 61. 106 (-) 58 Zuweisungsoperator 158 (=) 106 überladen 318.327 «88 I<nd»x