243
Fachhochschule für die Wirtschaft – FHDW – Hannover Objektorientiertes C++ für Einsteiger Skript zur gleichnamigen Veranstaltung Verfasser: Christoph Schulz E-Mail: [email protected] Copyright © 2004-2007 Fachhochschule für die Wirtschaft, Hannover

Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

  • Upload
    others

  • View
    3

  • Download
    0

Embed Size (px)

Citation preview

Page 1: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Fachhochschule für die Wirtschaft

– FHDW –

Hannover

Objektorientiertes C++ für Einsteiger

Skript zur gleichnamigen Veranstaltung

Verfasser:Christoph Schulz

E-Mail:[email protected]

Copyright © 2004-2007 Fachhochschule für die Wirtschaft, Hannover

Page 2: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,
Page 3: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger

InhaltsverzeichnisAbbildungsverzeichnis.................................................................................................VTabellenverzeichnis.................................................................................................... VI1 Überblick....................................................................................................................1

1.1 Ziele....................................................................................................................11.2 Leitsätze und Konventionen...............................................................................11.3 Aufbau................................................................................................................31.4 Entwicklungssystem...........................................................................................4

2 Die ersten Schritte......................................................................................................52.1 Der Weg vom Quelltext zur ausführbaren Datei................................................52.2 Die Entwicklungsumgebung.............................................................................. 62.3 Aller Anfang ist leicht......................................................................................102.4 Wie geht es weiter?.......................................................................................... 162.5 Übungen........................................................................................................... 16

3 Grundlegende Konzepte...........................................................................................173.1 Programm-Aufbau............................................................................................17

3.1.1 Übersetzungeinheiten............................................................................... 173.1.2 Die main-Funktion....................................................................................19

3.2 Lexikalische Elemente..................................................................................... 203.2.1 Kommentare............................................................................................. 203.2.2 Bezeichner................................................................................................ 213.2.3 Schlüsselwörter.........................................................................................223.2.4 Operatoren und Interpunktionszeichen.....................................................233.2.5 Zahlen....................................................................................................... 233.2.6 Zeichen und Zeichenketten.......................................................................253.2.7 Trenner..................................................................................................... 27

3.3 Namen und Entitäten........................................................................................273.3.1 Deklarationen, Definitionen und Header-Dateien....................................283.3.2 Variablen.................................................................................................. 323.3.3 Initialisierung............................................................................................323.3.4 Gültigkeitsbereiche...................................................................................333.3.5 Sichtbarkeit...............................................................................................35

3.4 Typen und Ausdrücke...................................................................................... 373.4.1 Zuweisungen (offene und verkappte).......................................................413.4.2 Fundamentale Datentypen........................................................................ 43

3.4.2.1 Datentypen für Zeichen und Zeichenketten......................................433.4.2.2 Datentypen für Ganzzahlen.............................................................. 483.4.2.3 Datentypen für Fließkommazahlen...................................................523.4.2.4 Datentyp für Wahrheitswerte............................................................543.4.2.5 Der leere Datentyp............................................................................ 55

3.4.3 Zusammengesetzte Datentypen................................................................ 553.4.3.1 Funktionstypen................................................................................. 563.4.3.2 Referenzen........................................................................................ 573.4.3.3 Zeiger................................................................................................ 583.4.3.4 Klassen..............................................................................................60

I

Page 4: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger

3.4.3.5 Felder................................................................................................ 603.4.3.6 Andere zusammengesetzte Datentypen............................................ 61

3.4.4 Konstanten................................................................................................623.4.5 Typ-Verträglichkeit und Konvertierungen............................................... 63

3.4.5.1 Typ-Verträglichkeit und implizite Typ-Konvertierungen................ 633.4.5.2 Explizite Typ-Konvertierungen (Casting)........................................ 66

3.4.6 Typ-Aliase................................................................................................ 673.5 Anweisungen....................................................................................................69

3.5.1 Ausdrücke.................................................................................................703.5.2 Deklarationen........................................................................................... 703.5.3 Fallunterscheidung und Selektion............................................................ 71

3.5.3.1 Fallunterscheidung............................................................................713.5.3.2 Selektion........................................................................................... 73

3.5.4 Schleifen................................................................................................... 743.5.4.1 Die while-Schleife............................................................................ 753.5.4.2 Die do-Schleife................................................................................. 763.5.4.3 Die for-Schleife................................................................................ 77

3.5.5 Sprünge.....................................................................................................793.5.6 Blöcke.......................................................................................................80

3.6 Funktionen........................................................................................................803.6.1 Das prozedurale Paradigma......................................................................813.6.2 (Klassische) Funktionen........................................................................... 823.6.3 Prozeduren oder Funktionen „ohne Wert“............................................... 843.6.4 Parameter und Argumente........................................................................843.6.5 Rekursion..................................................................................................88

3.7 Literaturempfehlungen..................................................................................... 913.8 Übungen........................................................................................................... 92

4 Die Welt der Objekte............................................................................................... 954.1 Grundlagen....................................................................................................... 95

4.1.1 Objekte, Nachrichten, Operationen und Methoden..................................954.1.2 Assoziationen und Aggregationen............................................................984.1.3 Gemeinsamkeiten, Schnittstellen und Polymorphie.................................984.1.4 Attribute..................................................................................................1014.1.5 Klassen................................................................................................... 1014.1.6 Erweiterung, Spezialisierung und Vererbung.........................................103

4.2 Ein erstes objektorientiertes Programm......................................................... 1054.3 UML (Unified Modeling Language)..............................................................107

4.3.1 Klassendiagramme................................................................................. 1084.3.2 Aktivitätsdiagramme.............................................................................. 109

4.4 Konkrete Datentypen: Daten und Methoden kapseln.................................... 1104.4.1 Problemstellung......................................................................................1104.4.2 Analyse-Phase........................................................................................ 111

4.4.2.1 Fachlexikon.....................................................................................1114.4.2.2 Fachklassen-Diagramm.................................................................. 111

4.4.3 Entwurfs-Phase.......................................................................................1134.4.3.1 Typen.............................................................................................. 113

II

Page 5: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger

4.4.3.2 Verhalten.........................................................................................1134.4.3.3 Zustände..........................................................................................1134.4.3.4 Fehler-Situationen...........................................................................1144.4.3.5 Resultierendes Klassendiagramm................................................... 114

4.4.4 Exkurs: Attribute und kompositionale Beziehungen..............................1154.4.5 Implementierungs-Phase........................................................................ 115

4.4.5.1 Klassen und Methoden................................................................... 1154.4.5.2 Definition der Klassen und Schnittstellen...................................... 1164.4.5.3 Implementierung der Operationen.................................................. 118

4.4.6 Exkurs: Zugriffsschutz........................................................................... 1214.4.7 Exkurs: const-Operationen und const-Parameter................................... 122

4.4.7.1 const-Operationen...........................................................................1224.4.7.2 const-Parameter.............................................................................. 124

4.4.8 Test-Phase...............................................................................................1264.5 Objekte erzeugen, zerstören, und leben lassen...............................................131

4.5.1 Konstruktoren und Initialisierung...........................................................1314.5.2 Destruktoren und RAII........................................................................... 1374.5.3 Kopien und die Sache mit der Lebensdauer........................................... 142

4.5.3.1 Der Kopier-Konstruktor..................................................................1434.5.3.2 Verhindern von Kopien.................................................................. 1464.5.3.3 Zuweisungen...................................................................................147

4.5.4 Temporäre Objekte.................................................................................1494.5.5 Dynamischer Speicher............................................................................151

4.5.5.1 Speicher belegen und freigeben......................................................1514.5.5.2 Der „leere“ Verweis........................................................................1534.5.5.3 Wenn kein Speicher mehr da ist..................................................... 1544.5.5.4 Dynamische Datenstrukturen..........................................................155

4.6 Abstrakte Datentypen: Erweiterbarkeit und Flexibilität erhöhen.................. 1614.6.1 (Abstrakte) Operationen und abstrakte Klassen..................................... 1614.6.2 Implementierung von Schnittstellen.......................................................1644.6.3 Polymorphie........................................................................................... 1684.6.4 Einfachvererbung................................................................................... 171

4.6.4.1 Redefinition einer Methode............................................................ 1724.6.4.2 Aufruf der Oberklasse.....................................................................1794.6.4.3 Vererbung und Polymorphie...........................................................1794.6.4.4 Konstruktoren in Vererbungshierarchien........................................1834.6.4.5 Destruktoren in Vererbungshierarchien..........................................184

4.6.5 Mehrfachvererbung................................................................................ 1854.6.5.1 Rauten & Co................................................................................... 1884.6.5.2 Virtuelle Basisklassen.....................................................................1894.6.5.3 Redefinition einer Methode und Dominanz....................................1924.6.5.4 Konstruktoren und Destruktoren bei Mehrfachvererbung..............193

4.6.6 Korrekte Anwendung von Vererbung.................................................... 1964.6.6.1 Beispiel 1: Rechteck und Quadrat.................................................. 1964.6.6.2 Beispiel 2: Elemente und Listen derselben.....................................1984.6.6.3 Zusammenfassung.......................................................................... 201

III

Page 6: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger

4.7 Literaturempfehlungen................................................................................... 2014.8 Übungen......................................................................................................... 201

5 Fehler erkennen und behandeln............................................................................. 2035.1 Grundlegendes................................................................................................2035.2 Erzeugen von Ausnahmen..............................................................................2045.3 Behandeln von Ausnahmen............................................................................2075.4 Ausnahmen während des Programmlaufs...................................................... 2095.5 Behandeln beliebiger Ausnahmen..................................................................2125.6 Erneutes Auswerfen von Ausnahmen............................................................ 2135.7 Ausnahme-Spezifikationen............................................................................ 2165.8 Ausnahme-Sicherheit und warum sie so wichtig ist...................................... 2205.9 Ausnahmen und RAII.................................................................................... 222

6 Entwurfsmuster...................................................................................................... 2276.1 Einführung......................................................................................................2276.2 Strukturelle Muster.........................................................................................227

6.2.1 Composite...............................................................................................2276.2.2 Decorator................................................................................................ 2276.2.3 Proxy.......................................................................................................227

6.3 Verhaltensmuster............................................................................................2276.3.1 Template Method....................................................................................2276.3.2 Iterator.................................................................................................... 2276.3.3 Observer................................................................................................. 2276.3.4 Strategy...................................................................................................2276.3.5 State........................................................................................................ 2276.3.6 Command............................................................................................... 227

6.4 Erzeugungsmuster.......................................................................................... 2276.4.1 Abstract Factory..................................................................................... 2286.4.2 Singleton.................................................................................................228

7 Überladung und Schablonen.................................................................................. 2297.1 Überladung..................................................................................................... 229

7.1.1 Überladung von Funktionen................................................................... 2297.1.2 Überladung von Operationen und Methoden......................................... 2297.1.3 Überladung von Operatoren................................................................... 229

7.2 Schablonen (Templates).................................................................................2298 Die Standard-Bibliothek........................................................................................ 231

8.1 Einführung......................................................................................................2318.2 Namensräume.................................................................................................2318.3 Datenstrukturen.............................................................................................. 2318.4 Algorithmen................................................................................................... 2318.5 Ein-/Ausgabe..................................................................................................231

Merksätze..................................................................................................................232Beispiele....................................................................................................................233Literaturverzeichnis.................................................................................................. 235

IV

Page 7: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger

AbbildungsverzeichnisAbbildung 1: Die einzelnen Phasen in der Software-Entwicklung.............................. 6Abbildung 2: Das Hauptfenster der Entwicklungsumgebung VC++........................... 7Abbildung 3: Ein neues Projekt in VC++, Teil 1......................................................... 8Abbildung 4: Ein neues Projekt in VC++, Teil 2......................................................... 9Abbildung 5: VC++ nach dem Anlegen des hello-Projekts......................................... 9Abbildung 6: Erzeugen eines neuen Editor-Fensters..................................................10Abbildung 7: Datei einem Projekt hinzufügen........................................................... 11Abbildung 8: Speichern des Arbeitsbereichs..............................................................11Abbildung 9: Übersetzen eines C++-Programms....................................................... 15Abbildung 10: Ausführen eines C++-Programms...................................................... 15Abbildung 11: Ausgaben des VC++-Übersetzers (wenn keine Fehler vorliegen)..... 16Abbildung 12: Der erste Testlauf................................................................................16Abbildung 13: Verteilte Deklarationen und Definitionen.......................................... 32Abbildung 14: Gültigkeitsbereiche............................................................................. 34Abbildung 15: Verdecken von Namen....................................................................... 36Abbildung 16: Verlustfreie Konvertierungen in C++.................................................65Abbildung 17: Verlustfreie Konvertierungen in VC++..............................................66Abbildung 18: Rekursion am Beispiel der Berechnung von 3! (3 Fakultät).............. 90Abbildung 19: System interagierender Objekte..........................................................95Abbildung 20: Video-Beispiel: Objekte und Beziehungen........................................ 96Abbildung 21: Video-Beispiel: Klienten, Dienstleister und Nachrichten.................. 96Abbildung 22: Video-Beispiel: Operationen und Methoden...................................... 97Abbildung 23: Video-Beispiel: Schnittstellen............................................................ 99Abbildung 24: Video-Beispiel: Attribute..................................................................101Abbildung 25: Video-Beispiel: Erweiterung einer Schnittstelle.............................. 103Abbildung 26: Video-Beispiel: Spezialisierung von Schnittstellen und Klassen.....104Abbildung 27: Das Video-Beispiel als UML-Klassendiagramm............................. 107Abbildung 28: Aktivitätsdiagramm zum do-Beispiel (3.5.4.2)................................ 109Abbildung 29: Fachklassen-Diagramm zur Queue-Aufgabe....................................112Abbildung 30: Vereinfachtes Fachklassen-Diagramm zur Queue-Aufgabe............ 112Abbildung 31: Klassendiagramm nach der Entwurfsphase...................................... 114Abbildung 32: Komposition anstatt von Attributen..................................................115Abbildung 33: Ausgabe der Queue-Tests................................................................. 131Abbildung 34: Stapel-Operationen........................................................................... 156Abbildung 35: Einfach verkettete Liste mit drei Elementen.................................... 156Abbildung 36: Entwurf einer Stapel-Klasse, die auf einer verketten Liste aufbaut. 156Abbildung 37: GraphicalObject-Schnittstelle...........................................................164Abbildung 38: GraphicalObject-Hierarchie..............................................................166Abbildung 39: Verschiedene Spielarten der Vererbung........................................... 172Abbildung 40: Ursprüngliche Anordnung der Elemente.......................................... 178Abbildung 41: Mögliche Anordnung nach instabiler Sortierung............................. 178Abbildung 42: Anordnung nach stabiler Sortierung.................................................178Abbildung 43: Raute bei Mehrfachvererbung.......................................................... 189Abbildung 44: Rauten, die keine sind.......................................................................190

V

Page 8: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger

Abbildung 45: Beispiel für Dominanz von Methoden..............................................192Abbildung 46: Spezialisierung zwischen Quadrat und Rechteck............................. 197Abbildung 47: Spezialisierung zwischen PointList und GraphicalObjectList......... 199

TabellenverzeichnisTabelle 1: Schlüsselwörter in C++..............................................................................23Tabelle 2: Operatoren und Interpunktionszeichen in C++..........................................24Tabelle 3: Operatoren nach ihrer Priorität sortiert......................................................39Tabelle 4: char-basierte Ein-/Ausgabeströme und ihre wchar_t-Pendanten...............45Tabelle 5: Relationale Operationen auf Zeichen........................................................ 46Tabelle 6: C++-Datentypen für Ganzzahlen............................................................... 48Tabelle 7: Rechenregeln bei ganzzahliger Division................................................... 50Tabelle 8: Rechenregeln bei ganzzahliger Division mit Rest.....................................50Tabelle 9: Vergleiche von ganzen Zahlen.................................................................. 51Tabelle 10: Garantierte und typische Wertebereiche von Fließkommazahlen........... 52Tabelle 11: Garantierte und typische Genauigkeiten von Fließkommazahlen........... 52Tabelle 12: Konvertierungen zwischen fundamentalen Datentypen.......................... 64

VI

Page 9: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Überblick

1 Überblick

1.1 ZieleDieses Skript hat es sich zum Ziel gesetzt, eine Einführung in die verbreitete Pro­grammiersprache C++ zusammen mit einer Einführung in die objektorientierte Soft­ware-Entwicklung zu kombinieren. Nach der gründlichen Durcharbeitung des Skripts sollten Sie mit den wichtigsten Sprachmitteln von C++ sowie grundlegenden Kennt­nissen in der objektorientierten Software-Entwicklung vertraut sein.

Angesprochen sind dabei Leser, die bereits grundlegende Programmiererfahrungen in einer anderen (aber nicht unbedingt objektorientierten) Programmiersprache ge­macht haben und nun C++ kennen lernen wollen. Aber auch wenn Sie bereits erste Erfahrungen mit C++ gemacht haben, sollten Sie hier Interessantes vorfinden. Vor­ausgesetzt wird lediglich, dass Sie grundlegende Programmier-Kenntnisse mitbrin­gen und Begriffe wie Programm, Algorithmus, Funktion, Prozedur und Variable zu­mindest intuitiv erklären können. Außerdem sollten Sie Ihr Computer-System bedienen und z. B. ein Programm starten oder einen Text-Editor benutzen können.

Im Folgenden wird bei Wörtern, die sowohl in einer weiblichen als auch männlichen Wortform existieren (z. B. Leserin oder Leser) die männliche Wortform verwendet. Fühlen Sie sich dadurch aber nicht ausgeschlossen, wenn Sie weiblich sind – das Skript würde sonst ziemlich unleserlich, wenn beide Formen ständig verwendet wür­den. (Und irgendeine Wahl muss der Autor schließlich treffen...)

1.2 Leitsätze und KonventionenEs ist immer schwierig, es sowohl Einsteigern als auch Lesern mit fortgeschrittenen Kenntnissen recht zu machen. Schließlich mögen Sie zu den Lesern gehören, die C++ bereits kennen und in einigen kleinen Projekten angewandt haben, aber mit dem OO-Paradigma1 nicht besonders gut vertraut sind. Umgekehrt programmieren Sie vielleicht seit Jahren in (anderen) objektorientierten Sprachen und möchten nun C++ kennen lernen. Um den verschiedenen Vorkenntnissen und Erwartungen der Leser gerecht zu werden, orientiert sich das Skript an folgenden Leitsätzen:

• Es lernt sich leichter an Beispielen. Neue Konzepte werden anhand von Bei­spielen eingeführt. Die Beispiele sind möglichst praxisnah gewählt, so dass der Nutzen vorgestellter Sprachkonzepte unmittelbar deutlich werden sollte.

• Übungen steigern den Lerneffekt. Am Ende eines jeden Kapitels finden sich Übungen, in denen Sie die dort erworbenen Kenntnisse anwenden und vertiefen können (bzw. sollten!). Dabei ist jeder Übung ein (geschätzter) Schwierigkeits­grad zwischen (*1) und (*5) zugeordnet, wobei Übungen mit höherem Schwie­rigkeitsgrad schwieriger sind als jene mit niedrigerem Schwierigkeitsgrad.2 Die Skala ist aber nicht linear, sondern eher exponentiell: Wenn Sie für eine (*1)-Übung etwa zehn Minuten benötigen, so brauchen Sie für eine (*2)-Übung unge­fähr eine Stunde und für eine (*3)-Übung vermutlich einen halben Tag. Natür­

1) „OO“ ist eine gängige Abkürzung für „objektorientiert“, die Sie häufiger antreffen werden.2) Diese Klassifizierung der Übungen wurde [Strou00] entnommen.

1

Zielsetzung

Leitsätze

viele Beispiele

viele Übungen

Zielgruppe und Voraussetzungen

Page 10: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Überblick Objektorientiertes C++ für Einsteiger

lich hängt dies stark von Ihren Vorkenntnissen und anderen Umständen3 ab, so dass Sie diese Angaben nur als Richtwert sehen sollten.

• Alles zur rechten Zeit. Abschnitte, die eher für Fortgeschrittene interessant sind oder besonders wichtige Informationen beinhalten, sind mit speziellen Symbolen am Seiten- oder Absatzrand gekennzeichnet:

Ein Absatz, der mit diesem Symbol gekennzeichnet ist, bedarf zum vollständigen Ver­ständnis fortgeschrittene C++-Kenntnisse. Insbesondere beinhalten diese Abschnitte häufig Bezüge zu späteren Abschnitten und Kapiteln. Anfänger sollten deshalb beim ersten Durcharbeiten des Skripts diese Abschnitte getrost überspringen. Fortgeschrittene Programmierer finden jedoch darin zusätzliche und wertvolle Informationen, die zu ei­nem tiefer greifenden Verständnis der dargestellten C++-Konzepte oder -Methoden füh­ren.

Dieses Symbol weist auf einen Abschnitt oder Absatz hin, der fortgeschrittene Techni­ken oder Methoden der objektorientierten Analyse, des objektorientierten Entwurfs oder der objektorientierten Programmierung darstellt. Er kann vom Anfänger beim ersten Durcharbeiten des Skripts ruhig „überlesen“ werden.

Genauso, wie die Welt „da draußen“ sich von unseren Idealen fast immer unterscheidet, gibt es auch Fallen und Tücken in der Software-Entwicklung, in die ein unbedarfter Ein­steiger hineintappen kann bzw. mit denen er nicht rechnet. Das Skript versucht, auf sol­che Situation mit einem Ausrufezeichen hinzuweisen. Sie sollten die auf diese Weise markierten Abschnitte besonders gründlich durcharbeiten.

Durch dieses Symbol wird der Leser darauf aufmerksam gemacht, dass der solcherart markierte Abschnitt spezielle Informationen für den kundigen Java-Programmierer be­reithält – seien es Ähnlichkeiten oder Unterschiede, Hilfen oder Warnungen. Sie können diese Abschnitte gefahrlos übergehen, wenn Sie keinerlei Erfahrungen in der Program­mierung mit Java gemacht haben.

• Merksätze. Wichtige Erkenntnisse werden in Form von kurzen und prägnanten Merksätzen formuliert. Diese Merksätze sind durchnummeriert und am Ende des Skripts in einem kleinen Verzeichnis zusammengefasst.

Schließlich hält das Skript gewisse Konventionen bezüglich der Darstellung ein:

• Generell gilt, dass Symbole am Seitenrand für den gesamten Abschnitt, Symbole am Absatzrand hingegen nur für den jeweiligen Absatz gelten. Dieser ist dann auch in etwas kleinerer Schrift gehalten, damit er sich besser vom restlichen Text unterscheidet und Anfang und Ende des durch das Symbol markierten Textes klar hervortreten. Beispiele für derart formatierte Absätze haben Sie bereits wei­ter oben kennen gelernt.

• C++-Quelltexte werden generell in nicht-proportionaler Schrift gedruckt, Schlüs­selwörter (3.2.3) zusätzlich zur besseren Hervorhebung fett formatiert. Kommen­tare (3.2.1) innerhalb von Quelltexten werden kursiv (und in proportionaler Schrift) gesetzt, um die Lesbarkeit der Code-Fragmente zu verbessern. Weiterhin sind die Zeilen aller Quelltexte durchnummeriert. Beispiel:

1 int fakultaet (int argument) // berechnet die Fakultät2 {

3) Es kann sein, dass Sie eine (*5)-Übung innerhalb einer halben Stunde lösen können, wenn Sie ge­eignete Werkzeuge benutzen, die Ihnen die Lösung der Aufgabe entscheidend erleichtern. Umge­kehrt werden Sie an einer (*1)-Übung vielleicht einen halben Tag lang sitzen, wenn Sie sich erst in Ihre Entwicklungsumgebung einarbeiten müssen.

2

Eile mit Weile

Konventionen

Aufgepasst!

OO für Fortge­schrittene

C++ für Fortge­schrittene

Merksätze

Vergleich mit Java

Page 11: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Überblick

3 return argument == 0 ? 1 : argument * fakultaet (argument – 1);

4 }

• Eingaben und Ausgaben von Programmen werden ebenfalls in nicht-proportio­naler Schriftart gesetzt. Beispiel: Press any key to continue

• Tasten und Tastenkombinationen werden in nicht-proportionaler Schrift und mit Kapitälchen gesetzt. Beispiel: F7, STRG+F5

• Dateinamen werden in nicht-proportionaler Schrift dargestellt. Beispiel: hel­lo.cpp

• Elemente von graphischen Oberflächen wie Menüpunkte, Texte innerhalb von Dialogen (etwa auf Schaltflächen) etc. werden kursiv dargestellt. Beispiel: File → New...

1.3 AufbauDas Skript ist folgendermaßen aufgebaut:

• Kapitel 1 lesen Sie gerade und enthält grundlegende Informationen zum Skript.

• In Kapitel 2 lernen Sie die Entwicklungsumgebung Microsoft Visual C++ 6.0 kennen und schreiben Ihr erstes C++-Programm. Dabei werden Sie mit den ers­ten Grundkonzepten von C++ vertraut gemacht.

Sie können dieses Kapitel überspringen, falls Sie bereits erste Erfahrungen in der Programmiersprache C++ gesammelt haben.

• Kapitel 3 geht näher auf die (nicht-OO-)Grundlagen von C++ ein. Funktionen, Variablen, Typen, Anweisungen und Ausdrücke werden ausführlich besprochen und deren typische Anwendung aufgezeigt. Zusätzlich wird der Datentyp string aus der C++-Standard-Bibliothek zur Speicherung und Manipulation von Zeichenketten eingeführt.

Dieses Kapitel sollten Sie lesen, wenn Sie nicht bereits über fundierte Kenntnisse in C++ verfügen. Insbesondere sollte der C-kundige4 Leser dieses Kapitel nicht überspringen, weil viele dort vorgestellten Konzepte sich in Details von denen in C unterscheiden.

• In Kapitel 4 lernen Sie das objektorientierte Paradigma kennen. Sie werden er­fahren, was Klassen, Objekte, Operationen und Methoden sind und wie Sie diese in C++ definieren und nutzen können. Sie werden über Schnittstellen, Vererbung und Delegation aufgeklärt und lernen, wie man diese Mittel nutzbringend anwen­det. Sie lernen, was Konstruktoren und Destruktoren sind und wie Sie Ihre Daten und Operationen vor unberechtigter Verwendung schützen können. Weiterhin geht Kapitel 4 auch auf die (dynamische) Speicherverwaltung ein.

Da dieses Kapitel das Zentrum des Skripts darstellt und sehr viele nützliche In­formationen zu objektorientierter Programmierung in C++ enthält, sollten Sie dieses Kapitel unbedingt lesen. Es baut auf den grundlegenden Konzepten aus

4) C ist eine Programmiersprache, die als Grundlage für die Entwicklung von C++ diente.

3

Aufbau

Allgemeines

das erste Pro­gramm

Grundlagen von C++

Objektorientie­rung und Spei­cherverwaltung

Page 12: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Überblick Objektorientiertes C++ für Einsteiger

Kapitel 3 auf, so dass Sie diese verstanden haben sollten, bevor Sie dieses Kapi­tel durcharbeiten.

• Kapitel 5 erläutert, wie Fehler-Situationen in C++ geeignet behandelt werden können. Es stellt das Konzept von Ausnahmen vor und erklärt, wie diese dazu genutzt werden können, Informationen von der Fehler-Quelle zum Fehler-Be­handler transferieren zu können.

• Kapitel 6 führt Sie in die Welt der Entwurfsmuster ein. Sie erfahren, wie Sie mit Mustern existierende Lösungen in neuen Kontexten wiederverwenden können und bekommen die wichtigsten Entwurfsmuster vorgestellt.

Dieses Kapitel baut auf Kapitel 3 und natürlich Kapitel 4 auf und sollte erst dann durchgearbeitet werden, wenn Sie mit den dort behandelten Konzepten hinläng­lich vertraut sind und zumindest die Hälfte aller dort enthaltenen Übungen er­folgreich absolviert haben.

• In Kapitel 7 lernen Sie das Überladen von Funktionen und Operationen sowie Schablonen (Templates) als weitere C++-Abstraktionskonzepte kennen. Um den Rahmen nicht zu sprengen, wird insbesondere auf letztere nur kurz eingegangen.

Dieses Kapitel baut ebenfalls auf den Grundlagen von Kapitel 3 und 4 auf.

• Kapitel 8 führt Sie in Namensräume und die C++-Standard-Bibliothek ein und erklärt Ihnen, warum es häufig nicht sinnvoll ist, das Rad ständig neu zu erfin­den. Sie lernen das Ein-/Ausgabe-System von C++ kennen und erfahren, wie Container, Iteratoren und Algorithmen zusammenspielen.

Da die C++-Standard-Bibliothek so ziemlich alle Sprachmittel ausschöpft, die C++ anzubieten hat, ist es von Vorteil, wenn Sie die vorherigen Kapitel alle durchgearbeitet haben.

1.4 EntwicklungssystemZum Thema Entwicklungssystem: Das Skript kann sich naturgemäß nicht mit den verschiedenen Rechner-Typen, Betriebssystemen und C++-Entwicklungsumgebun­gen befassen, die zur Zeit existieren. Aus mehreren Gründen wurde in diesem Skript Microsoft Visual C++ 6.0 (im Folgenden einfach mit VC++ abgekürzt) unter dem Betriebssystem Microsoft Windows als Entwicklungssystem ausgewählt, hauptsäch­lich deshalb, weil dieses Produkt in der Industrie häufig eingesetzt wird5 und damit quasi einen De-facto-Standard darstellt. Entwickler, die mit einem anderen System bzw. einer anderen C++-Entwicklungsumgebung arbeiten, sollten jedoch problemlos die in dem Skript benutzten Beispiele auf „ihr“ System und „ihre“ Entwicklungs­werkzeuge übertragen können. Sie können in diesem Fall die Abschnitte, die sich ausschließlich mit VC++ befassen, problemlos überspringen.

Diese Abschnitte sind zur besseren Unterscheidung mit dem nebenstehenden Symbol gekennzeichnet.

5) und zwar immer noch, obwohl inzwischen bereits der dritte Nachfolger existiert!

4

die Wahl der Ent­wicklungsumge­bung

Entwurfsmuster

Überladung und Schablonen

Namensräume und C++-Stan­dard-Bibliothek

Fehlerbehand­lung

Page 13: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die ersten Schritte

2 Die ersten SchritteIn diesem Kapitel lernen Sie, welche Aufgabe eine Entwicklungsumgebung hat und was der Entwicklungsprozess ist. Sie erfahren, wie Sie in VC++ ein Projekt erzeu­gen, den Quelltext-Editor starten und Quellen Projekten zuordnen. Schließlich schreiben Sie Ihr erstes C++-Programm, übersetzen es und führen es aus.

2.1 Der Weg vom Quelltext zur ausführbaren DateiWenn Sie vielleicht noch nie mit einer Entwicklungsumgebung gearbeitet haben, fra­gen Sie sich, wozu Sie diese überhaupt brauchen. Nun, eine solche Entwicklungsum­gebung wird für gewöhnlich drei zentrale Funktionen in sich vereinen, die allesamt während der Entwicklung und dem Test eines Programms von Bedeutung sind:

(1) Erzeugen/Bearbeiten von Quelltexten und anderen Ressourcen: Zuerst muss in einer Programmiersprache6 das zu erstellende Programm „niedergeschrieben“ werden. Dies unterstützt eine Entwicklungsumgebung, indem sie Funktionen zum Erzeugen und Bearbeiten von Quellen zur Verfügung stellt. Als Quellen werden alle Ressourcen bezeichnet, die vom Programmierer in die Entwicklung des Programms eingebracht werden und nicht von der Entwicklungsumgebung selbstständig erzeugt werden können. Außer Programm-Quelltexten gehören hierzu beispielsweise auch Hilfe-Texte, Bilder, Videos und dergleichen mehr. Dieser Vorgang des „Niederschreibens“ wird häufig Kodieren genannt.

(2) Übersetzen der Quelltexte und Ressourcen in eine lauffähige Datei: Die in Schritt 1 erzeugten Quellen sind jedoch in den meisten Fällen nicht direkt aus­führbar, d. h. es muss eine geeignete Übersetzung stattfinden. Dabei werden alle Quellen auf Korrektheit geprüft und in Maschinen-Befehle übersetzt. Schließlich werden alle übersetzten Quellen zu einem ausführbares Programm gebunden. Den Vorgang des Übersetzens nennt man auch kompilieren (= zusammenstel­len), den des Bindens auch linken (engl. to link = binden, verknüpfen).

(3) Ausführen des übersetzten Programms (zu Testzwecken): Konnte das entwi­ckelte Programm übersetzt werden, heißt das noch lange nicht, dass es auch funktioniert. In einem oder mehreren Testläufen muss geprüft werden, ob die Anforderungen an das Programm erfüllt worden sind. Eine Entwicklungsumge­bung enthält in der Regel nicht nur Funktionen zum normalen Ausführen des übersetzten Programms, sondern auch zur Fehlersuche und -behebung, das ge­meinhin als Debugging bezeichnet wird.

Bevor jedoch diese drei Phasen durchlaufen werden (und somit eine Entwicklungs­umgebung den Programm-Entwickler unterstützt), müssen die Anforderungen an das Programm analysiert und die einzelnen Komponenten des Programms quasi „auf dem Reißbrett“ entworfen werden. Beispielhaft führen wir dies in Abschnitt 4.4 durch. Das Beispiel zeigt insbesondere, dass Software-Entwicklung aus mehr besteht als nur aus dem Eintippen von Sprach-Konstrukten in einem Editor innerhalb einer Entwicklungsumgebung.

6) Es gibt auch Programme, die in mehreren Programmiersprachen entwickelt werden. Dies wird je­doch innerhalb dieses Skriptes nicht weiter verfolgt.

5

Warum eine Ent­wicklungsumge­bung?

Analyse und Ent­wurf

Lernziele

Page 14: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die ersten Schritte Objektorientiertes C++ für Einsteiger

Die einzelnen Phasen des Entwicklungsprozesses veranschaulicht zusammenfassend Abbildung 1. Diese Phasen bauen aufeinander auf (= inkrementell) und werden wäh­rend der Software-Entwicklung immer wieder durchlaufen (= iterativ). Deshalb spricht man auch vom iterativ-inkrementellen Entwicklungsprozess. (Wenn Sie sich fragen, warum Sie immer wieder die Analyse-Phase durchlaufen soll(t)en, dann fra­gen Sie sich auch mal, wie viele Programme Sie benutzen, die vielleicht Probleme lösen, aber die falschen...)

2.2 Die EntwicklungsumgebungNach der langen Vorrede geht es jetzt endlich zur Sache! Zuallererst müssen Sie Ihre Entwicklungsumgebung starten, damit Sie überhaupt in der Lage sind, C++-Pro­gramme einzugeben, zu übersetzen und laufen zu lassen.

Starten Sie also VC++ über die entsprechende Verknüpfung. Abbildung 2 zeigt das Hauptfenster von VC++, das sich Ihnen nach dem Start präsentiert. (Falls Sie über eine andere als die hier beschriebene englische Version des Programms verfügen, müssen Sie sich die Menüpunkte und Dialog-Texte geeignet „übersetzen“.)

Es folgt eine kurze Beschreibung der wichtigsten Bereiche:

• Bereich 1 ist die Titelzeile der Entwicklungsumgebung. Sie enthält (momentan nicht sichtbar) als wichtigste Information den Namen der gerade bearbeiteten Datei.

• Bereich 2 enthält die Menüleiste, über welche die meisten Aktionen vorgenom­men werden.

• Bereich 3 enthält Werkzeugleisten. Diese sind bebilderte „Abkürzungen“ zu ein­zelnen Menüpunkten.

• Bereich 4 ist der sogenannte Arbeitsbereich. In ihm werden später die zu einem Projekt gehörenden Dateien und Ressourcen dargestellt. Momentan ist er leer, weil noch kein Projekt erzeugt oder geöffnet worden ist.

6

Aller Anfang ist leicht

die wichtigsten Bereiche

Entwicklungspro­zess

Abbildung 1: Die einzelnen Phasen in der Software-Entwicklung

int do_it() {return 42;}

int main () { return do_it();}

int main () { return 0;}

0110101110100101010010011010Analyse

EntwurfRealisierung

Übersetzung

Test

Page 15: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die ersten Schritte

• Bereich 5 zeigt Ausgaben und Meldungen an, die während der Arbeit mit VC++ anfallen. In der ersten und wichtigsten Lasche Build werden Warnungen und Fehler angezeigt, die bei der Analyse Ihrer Programme durch die Entwicklungs­umgebung erkannt werden.

• Bereich 6 bietet Platz für alle anderen Fenster, insbesondere Editor-Fenster zum Bearbeiten von Quelltext-Dateien.

• Bereich 7 bildet die Statuszeile, welche alle Arten von Informationen enthält, die während der Arbeit mit der Entwicklungsumgebung anfallen. Dies könne Kon­text-sensitive Informationen sein (etwa ein erläuternder Text zu einem ausge­wählten Menüpunkt), das Ergebnis einer vorherigen Operation oder einfach nur eine Statusinformation (etwa „Ready“ wie in Abbildung 2).

Normalerweise besteht ein (C++-)Programm nicht lediglich aus einer Datei. Oftmals sind viele Dateien zur Entwicklung eines Programms erforderlich. In der Regel hat eine Aufteilung des Programms in mehrere Dateien folgende Vorteile:

• einzelne Programmteile lassen sich besser in anderen Programmen wiederver­wenden, wenn sie in separaten Dateien existieren

• der Quelltext wird überschaubarer, weil die Dateigröße im Schnitt kleiner ist

• die einzelnen Dateien lassen sich je nach Funktion in unterschiedliche Verzeich­nisse einordnen, was ebenfalls für mehr Übersichtlichkeit sorgt

7

Programme be­stehen aus vielen Quellen

Abbildung 2: Das Hauptfenster der Entwicklungsumgebung VC++

Page 16: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die ersten Schritte Objektorientiertes C++ für Einsteiger

• innerhalb eines Teams lassen sich Dateien und Verzeichnisse einzelnen Team-Mitgliedern zuordnen, so dass verschiedene Entwickler nicht an denselben Datei­en gleichzeitig arbeiten müssen (was oftmals zu Problemen führen kann)

• einige Ressourcen lassen sich in der Regel gar nicht oder nur sehr mühsam in ei­nen C++-Quelltext einbetten, etwa Bilder und Videos

Um für ein bestimmtes Programm alle zusammengehörigen Quellen zu verwalten, existieren in VC++ sogenannte Projekte. Ein Projekt ist eben eine solche Zusammen­stellung aller für ein Programm benötigten Quellen. Um die Entwicklung eines Pro­gramms zu beginnen, müssen Sie in VC++ also zuerst ein entsprechendes Projekt an­legen. Dies erledigen Sie einfach über den Menüpunkt File → New.... Es öffnet sich ein Dialog, in dem Sie folgende Änderungen vornehmen sollten (Abbildung 3):

• Wählen Sie die Lasche Projects aus (falls es nicht schon der Fall ist).

• Wählen Sie aus der Liste an möglichen Projekt-Typen Win32 Console Applicati­on aus.

• Wählen Sie bei Location einen gültigen Pfad aus, unter dem Ihr Projekt angelegt werden soll.

• Wählen Sie hello bei Project name als Namen für Ihr Projekt. Das Projekt wird in einem eigenen Verzeichnis unterhalb des bei Location angegebenen Ver­zeichnisses gespeichert (der Location-Pfad wird automatisch angepasst).

• Schließen Sie den Dialog über die OK-Schaltfläche.

Sie sollten jetzt einen weiteren Dialog zu Gesicht bekommen (Abbildung 4). Bitte bestätigen Sie in diesem Dialog über die Finish-Schaltfläche die aktuelle Auswahl An empty project, um ein leeres Projekt zu erstellen. Den nachfolgenden Dialog be­stätigen Sie bitte ebenfalls (er gibt Ihnen noch eine letzte Möglichkeit, die Erstellung des Projekts abzubrechen).

8

Quellen werden in Projekten zu­sammengefasst

Abbildung 3: Ein neues Projekt in VC++, Teil 1

Anlegen eines Projekts

Page 17: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die ersten Schritte

Ihr Hauptfenster sollte danach in etwa so aussehen wie in Abbildung 5.

Sollten Sie im Arbeitsbereich etwas anderes angezeigt bekommen, so liegt das daran, dass die falsche Lasche ausgewählt ist. Bitte wählen Sie die Lasche FileView aus und klappen die Liste der Dateien für das hello-Projekt auf.

Wie Sie sehen, sehen Sie nichts – immerhin sind unserem Projekt noch keine Quel­len zugeordnet. Das ändert sich jedoch im nächsten Abschnitt, in welchem Sie Ihr erstes C++-Programm schreiben werden. Allerdings können Sie noch nichts eintip­pen, weil kein Editor-Fenster existiert. Hierzu klicken Sie am einfachsten auf die ers­

9

Abbildung 5: VC++ nach dem Anlegen des hello-Projekts

Abbildung 4: Ein neues Projekt in VC++, Teil 2

Anlegen einer Quelltext-Datei

Page 18: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die ersten Schritte Objektorientiertes C++ für Einsteiger

te Schaltfläche in der obersten Werkzeugleiste (Abbildung 6), die ein leeres, unbe­nanntes Text-Fenster erzeugt, so dass Sie mit dem folgenden Schritt weitermachen können.

2.3 Aller Anfang ist leichtWenn Sie die erste Hürde gemeistert haben, haben Sie jetzt einen Texteditor o. ä. vor sich und sind in der Lage, C++-Quelltext einzugeben. Bitte tippen Sie den folgenden C++-Programmtext ein:

1 /*** Beispiel hello.cpp ***/2 #include <istream> // für die Eingabe3 #include <ostream> // für die Ausgabe4 #include <iostream> // für die Objekte cin und cout5 #include <string> // für Zeichenketten-Verarbeitung6 using namespace std;78 string liesName ()9 {

10 cout << "Bitte gib deinen Namen ein: ";11 string name;12 cin >> name;13 return name;14 }1516 void begruesse (string name)17 {18 cout << "Hallo " << name << "!" << endl;19 }2021 int main ()22 {23 cout << "Mein erstes C++-Programm" << endl;2425 string meinName = liesName ();26 begruesse (meinName);2728 return 0;29 }

Bitte speichern Sie nun den Quelltext in VC++ unter dem Dateinamen hello.cpp ab. Dazu drücken Sie am einfachsten die Tastenkombination STRG+S und geben den ge­nannten Dateinamen ein. Danach ist der Quelltext zwar gespeichert, die Datei ist jedoch nicht automatisch dem Projekt zugeordnet. Dazu müssen Sie im Arbeitsbereich-Fenster zu hello files das Kontextmenü öffnen und dort den Menü-Eintrag Add Files to Project... wählen (Abbildung 7). Wenn Sie dann die eben erstellte hello.cpp wäh­len, fügt sie VC++ zu den Quellen des Projekts hinzu. Vergessen Sie danach nicht, den

10

das erste Pro­gramm in C++

Abbildung 6: Erzeugen eines neuen Editor-Fensters

Speichern und Zuordnen

Page 19: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die ersten Schritte

Arbeitsbereich über den Menüpunkt File → Save Workspace ebenfalls zu speichern (Abbildung 8).

Analysieren wir das Programm nun Stück für Stück:

11

Abbildung 7: Datei einem Projekt hinzufügen

Abbildung 8: Speichern des Arbeitsbereichs

Page 20: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die ersten Schritte Objektorientiertes C++ für Einsteiger

• Zeile 1: Diese und die nächsten vier Zeilen demonstrieren Ihnen, wie Kommen­tare in den Quelltext integriert werden können. Der Übersetzer ignoriert alles zwischen /* und */ sowie alles zwischen // und dem Zeilenende. Somit lässt sich in diesem Bereich insbesondere der Quelltext kommentieren und dokumen­tieren.

• Zeilen 2 bis 5: Hier werden über sogenannte Präprozessor-Direktiven7 Entitäten zur Ein-/Ausgabe und zur Zeichenketten-Verarbeitung aus der C++-Standard-Bi­bliothek (8) für das Programm verfügbar gemacht. Die C++-Standard-Bibliothek ist eine vom C++-Standard wohldefinierte Ansammlung von nützlichen Funktio­nen (3.6), Objekten (4.5), Konstanten (3.4.4), Variablen (3.3.2), Typen (3.4), Schablonen (7.2) und noch einigen anderen „Dingen“, auf die wir im weiteren Verlauf nicht näher eingehen (können).

• Zeile 6: Diese Namensraum-Direktive (8.2) bewirkt, dass die über die #inclu­de-Direktiven eingebundenen Elemente der C++-Standard-Bibliothek ohne wei­tere Qualifizierung (4.4.5.2) direkt zur Verfügung stehen. Ansonsten müssten Sie in Ihrem Programm vor jeder Verwendung eines Elements das Präfix std:: schreiben (also beispielsweise std::cout statt nur cout), was bei häufiger Benutzung sicherlich mühsam ist. Somit handelt es sich bei dieser Direktive ge­nau genommen nur um eine Abkürzung. Sie werden noch sehen, dass C++ viele weitere Abkürzungen für „Schreibfaule“ in petto hat.

Sie sehen außerdem, dass diese Direktive mit einem Semikolon (;) beendet wird. Das Semikolon wird in C++ sehr oft gebraucht, um etwas abzuschließen, etwa hier das Ende der Direktive. Dies wird benötigt, weil C++ keine Zeilen-ori­entierte Programmiersprache ist. Dies meint, dass Sie alle Sprachmittel von C++ beliebig auf die Zeilen „verteilen“ können (Hauptsache, sie stehen „logisch hin­tereinander“). Sie könnten also im obigen Beispiel die drei Wörter using, namespace und std untereinander schreiben. Dies unterscheidet C++ von Zeilen-orientierten Sprachen wie z. B. BASIC. Damit der C++-Übersetzer aber dann erkennen kann, wo die Direktive zu Ende ist – schließlich kann er sich nicht am Ende der Zeile orientieren – benötigt er das Semikolon.

• Zeile 8: Hier wird eine Funktion mit dem Namen liesName definiert, die eine Zeichenkette zurückliefert. Funktionen sind in C++ – wie auch in anderen Pro­grammiersprachen – im Prinzip lediglich benannte Anweisungsblöcke, die einen Wert zurückgeben. Wie Sie sehen, gibt es für die Definition einer Funktion in C++ kein explizites Schlüsselwort, anders als beispielsweise in (Visual) BASIC oder Pascal. Diese Eigenart von C++, Schlüsselwörter möglichst sparsam zu ver­wenden, wird Ihnen im Verlauf des Skripts noch an einigen anderen Stellen be­gegnen.

• Zeile 9: Hier fängt der Anweisungsblock an, der zur Funktion liesName ge­hört.

7) die aus Platzgründen nicht weiter in diesem Skript behandelt werden

12

Präprozessor und Kommentare

Namensräume

Definieren einer Funktion

Semikolon

Page 21: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die ersten Schritte

• Zeile 10: Über diese Anweisung wird die Zeichenkette "Bitte gib deinen Namen ein: " auf der Konsole ausgegeben. cout ist hierbei ein (vordefi­niertes) Objekt, das einen Ausgabe-Strom repräsentiert. Es gibt noch andere vor­definierte Ströme, und Sie werden in Kapitel 8 lernen, wie Sie selbst Objekte zur Benutzung von Ein- und Ausgabe-Strömen erzeugen und benutzen. Die Anwei­sung ist im Grunde genommen ein Ausdruck (3.4) mit zwei Operanden (dem Ob­jekt cout und der Zeichenkette), die über den Operator << verknüpft sind.

• Zeile 11: Hier wird einfach eine Variable definiert, die eine Zeichenkette aufneh­men kann. Der Typ string, der dafür verwendet wird, wird von der C++-Stan­dard-Bibliothek (8.3) bereitgestellt. Die Variable wird in der nächsten Zeile ge­braucht und nimmt den einzulesenden Namen auf.

• Zeile 12: Die Anweisung ähnelt der in Zeile 10, bloß dass hier ein Eingabe-Strom (cin) statt eines Ausgabe-Stroms involviert ist und dass der Operator ein anderer ist (nämlich >>). Die Zeile bewirkt, dass eine Zeichenkette von der Kon­sole eingelesen und in die Variable name gespeichert wird.

• Zeile 13: Der eingelesene Name wird über die return-Anweisung an den Auf­rufer der Funktion zurückgegeben. Eine Funktion gibt immer einen Wert zurück (es sei denn, eine Ausnahme wird ausgeworfen, siehe hierzu Kapitel 5). Die re­turn-Anweisung gibt C++-Funktionen die Möglichkeit, eben dieses zu tun. Daraus folgt, dass (außer bei C++-Ausnahmen, siehe oben) immer mindestens eine return-Anweisung innerhalb einer Funktion existieren muss.

• Zeile 14: Hier wird der Anweisungsblock der Funktion liesName beendet. Pendant zu Zeile 9.

• Zeile 16: Hier wird eine Prozedur begruesse definiert. Prozeduren sind wie Funktionen benannte Anweisungsblöcke, nur dass sie keinen Wert zurückgeben, der vom Aufrufer weiterverarbeitet werden kann. Dies wird durch das Schlüssel­wort void (3.4.2.5) angezeigt. Um dennoch einen Zweck zu erfüllen, produzie­ren Prozeduren Seiteneffekte. Ein Seiteneffekt ist eine Veränderung des Pro­gramm-Zustands dergestalt, dass er die Ausführung des Programms oder dessen sichtbare Ergebnisse nachhaltig beeinflusst. Im oben angegebenen Programm be­wirkt ein Aufruf der Prozedur begruesse, dass ein Text auf der Konsole aus­gegeben wird. Der weggefallene „Zwang“ zur Rückgabe eines Wertes drückt sich auch darin aus, dass eine C++-Prozedur (wie im obigen Beispiel) in der Re­gel keine return-Anweisung enthält.8

Ebenfalls anders als bei der Funktion liesName ist die Verwendung von Para­metern. Wenn eine Prozedur oder Funktion Parameter in der Definition angibt, müssen diese Parameter beim Aufruf der Prozedur oder Funktion mit Werten be­legt werden, die dann in der Prozedur oder Funktion verwendet werden können. Im obigen Beispiel wird der Prozedur eine Zeichenkette übergeben, die innerhalb der Prozedur über den Namen name angesprochen werden kann. Die Parameter in der Definition der Prozedur oder Funktion nennt man Formalparameter oder

8) Und wenn sie doch eine enthält, hat sie keinen Ausdruck, sieht also so aus: return;

13

Ausgabe-Ströme

Definieren einer Variable

Eingabe-Ströme

Rückgabewert ei­ner Funktion

Definieren einer Prozedur; Seiten­effekte

Parameter und Argumente

Page 22: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die ersten Schritte Objektorientiertes C++ für Einsteiger

einfach Parameter, die Werte beim Aufruf der Prozedur oder Funktion Aktualpa­rameter oder Argumente. Formalparameter sind eigentlich nur Variablen, die beim Aufruf der Prozedur oder Funktion mit vom Aufrufer festgelegten Aus­drücken belegt werden.

Übrigens werden in C++ Prozeduren und Funktionen nicht so unterschieden, wie wir es hier im Skript der Klarheit willen halten. In C++ werden Prozeduren für gewöhnlich einfach als void-Funktionen bezeichnet. Um die ewig wiederkeh­rende und holprige Phrase „Prozedur oder Funktion“ zu vermeiden, werden wir ab jetzt nur von „Funktion“ sprechen und Prozeduren dabei implizit einschlie­ßen. Wenn etwas wirklich nur für Funktionen gilt, werden Sie gesondert darauf hingewiesen.

• Zeile 17: siehe Zeile 9

• Zeile 18: Hier werden – ähnlich wie in Zeile 10 – Daten in den cout-Ausgabe-Strom geschrieben. Allerdings werden hier mehrere Zeichenketten hintereinan­der weggeschrieben, wovon einige unveränderliche Konstanten oder Literale sind (z. B. "Hallo ") und eine einen Parameter darstellt (name). Das Objekt endl, das am Ende noch ausgegeben wird, entspricht einem Zeilenumbruch (end of line) und stellt sicher, dass zukünftige Ausgaben am Anfang der folgen­den Zeile platziert werden.

• Zeile 19: siehe Zeile 14

• Zeile 21: Jedes C++-Programm hat eine Funktion main, die bei der Ausführung des Programms vom Betriebssystem9 aufgerufen wird. Sie sehen, aller C++-Code befindet sich in Funktionen, auch der für das Hauptprogramm. Die Funk­tion main wird immer so definiert, dass sie einen ganzzahligen (= Typ int, s. 3.4.2.2) Wert zurückgibt, der vom Betriebssystem ausgewertet werden kann. Ty­pischerweise bedeutet der Wert 0 „alles in Ordnung“, während andere Werte für Fehler bei der Programm-Abarbeitung stehen. C++ legt die Bedeutung des von main zurückgegebenen Wertes allerdings nicht fest, so dass Sie hier völlig freie Hand haben (bzw. sich an die Richtlinien und Erwartungen Ihres Betriebssys­tems halten sollten!)

• Zeile 22: siehe Zeile 9

• Zeile 23: siehe Zeile 10 und Zeile 18

• Zeile 25: Hier passiert zweierlei. Zum einen wird eine Zeichenketten-Variable mit dem Namen meinName definiert und steht von diesem Zeitpunkt an zur Verfügung (zu Gültigkeitsbereichen und Sichtbarkeit siehe 3.3.4 und 3.3.5). Zum anderen wird diese Variable gleich mit einem Wert initialisiert (= vorbelegt), nämlich mit dem Ergebnis des Aufrufs der Funktion liesName. Sie sehen, dass die Funktionsdefinition und der Funktionsaufruf ziemlich ähnlich sind: Charak­teristisch sind die beiden runden Klammern, die an beiden Stellen (Definition und Aufruf) dem Übersetzer mitteilen, dass eine Funktion definiert bzw. aufge­rufen wird.

9) genauer: von der C++-Laufzeitbibliothek

14

Zeilenumbrüche bei der Ausgabe

main-Funktion

Initialisierung von Variablen und Aufruf von Funktionen

Page 23: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die ersten Schritte

• Zeile 26: An dieser Stelle wird nun die Funktion begruesse aufgerufen und ihr als Argument die Zeichenkette übergeben, die vorher von der Funktion liesName zurückgeliefert wurde. Diese Zeichenkette ist innerhalb der Funk­tion begruesse über den Parameter name zu erreichen. Wie Sie sehen, wer­den etwaige Argumente innerhalb der Klammern angegeben. Wenn mehrere Ar­gumente notwendig sind, werden diese durch Kommata getrennt. Näheres zum Aufruf von Funktionen finden Sie in Abschnitt 3.6.

Wenn Sie das Skript bis jetzt aufmerksam gelesen haben oder bereits Erfahrun­gen in C++ besitzen, dann werden Sie erkennen, dass Sie die beiden Zeilen 25 und 26 zusammenfassen und die Variable meinName „loswerden“ können. Sie müssen dazu nur das Ergebnis der Funktion liesName direkt an die Prozedur begruesse übergeben, etwa so:

25 begruesse (liesName ());

• Zeile 28: Wie bereits erwähnt erfordert die main-Funktion einen ganzzahligen Rückgabewert. Wir nehmen an, dass das Programm erfolgreich seinen Dienst (sprich: seine Ausgaben) getan hat und geben den Wert 0 zurück, um dem Auf­rufer den Erfolg mitzuteilen.

• Zeile 29: siehe Zeile 14Mühsam haben Sie sich durch Ihr erstes Programm „gekämpft“, nun soll Ihre Mühe auch belohnt werden: Über die Tastenkombination F7 oder den Menüpunkt Build → Build hello.exe können Sie Ihr Programm übersetzen (Abbildung 9), anschließend mit STRG+F5 oder dem Menüpunkt Build → Execute hello.exe (Abbildung 10) ausführen. Sobald der Übersetzer fertig ist, können Sie seine Meldungen im Ausgabe-Fenster be­trachten. Im Idealfall hat er keine Fehler gefunden und Ihr Programm anstandslos über­setzt (Abbildung 11). Abbildung 12 zeigt, wie die Ausgabe Ihres Programm nach einem ersten Testlauf aussehen kann. Übrigens wird die etwas seltsam anmutende Zeile Press any key to continue von VC++ generiert und nicht von unserem C++-Programm.

15

Übergabe von Ar­gumenten

Übersetzung und Ausführung

Abbildung 9: Übersetzen eines C++-Programms

Abbildung 10: Ausführen eines C++-Programms

Page 24: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die ersten Schritte Objektorientiertes C++ für Einsteiger

2.4 Wie geht es weiter?Sie haben beim Studieren des C++-Programms aus diesem Kapitel bereits einige Sprachkonstrukte von C++ kennen gelernt. Das folgende Kapitel beschäftigt sich nä­her mit den grundlegenden Sprachmitteln von C++, mit Ausnahme der objektorien­tierten Konstrukte, die in Kapitel 4 vorgestellt werden.

2.5 ÜbungenÜ1 (*3) Machen Sie sich mit Ihrer Entwicklungsumgebung vertraut! Finden Sie

heraus, wie Sie Quelltexte erstellen, laden, speichern und übersetzen können!

Ü2 (*1) Lernen Sie die „Sprache“ Ihres Übersetzers kennen: Provozieren Sie durch gezielte „Tippfehler“ einen Abbruch des Übersetzers und machen Sie sich mit seinen Meldungen und deren Aufbau vertraut! Prüfen Sie, ob Ihre Entwick­lungsumgebung die Meldungen des Übersetzers automatisch auswertet und Ih­nen z. B. durch Doppelklick o. ä. auf eine Fehlermeldung den fehlerhaften C++-Quelltext markiert!

Ü3 (*2,5) Wenn Sie bei Ihrem obigen Programm einen Namen mit Leerzeichen (etwa Vor- und Nachname, durch ein Leerzeichen getrennt) eingeben, gibt Ih­nen das Programm lediglich das Wort bis zum ersten Leerzeichen als Begrü­ßung zurück. Dies liegt in der Arbeitsweise des verwendeten >>-Operators (Zeile 12) zum Einlesen der Daten begründet. Es gibt jedoch eine Funktion getline, die das Gewünschte leistet und alles inklusive Leerzeichen einliest. Schauen Sie in der Dokumentation Ihres Compilers nach und bauen Sie Ihr Pro­gramm so um, dass es getline statt dem >>-Operator zum Einlesen des Na­mens verwendet. Übersetzen und testen Sie dann das Programm, sobald es kei­ne Syntax-Fehler mehr enthält!

16

Abbildung 11: Ausgaben des VC++-Übersetzers (wenn keine Fehler vorliegen)

Abbildung 12: Der erste Testlauf

Page 25: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

3 Grundlegende KonzepteDieses Kapitel vermittelt Ihnen grundlegende C++-Konzepte, die zum Verständnis der nachfolgenden Kapitel wichtig sind.

3.1 Programm-AufbauIhr erstes C++-Programm haben Sie nun auf jeden Fall geschafft. Wir wollen uns jetzt den grundlegenden Aufbau von C++-Programmen anschauen.

3.1.1 ÜbersetzungeinheitenJedes C++-Programm besteht aus mindestens einer sogenannten Übersetzungsein­heit. Eine Übersetzungseinheit ist – wie der Name suggeriert – ein „Stück Quelltext“, das der Übersetzer (oder Compiler) in einem Rutsch verarbeitet. Diese Übersetzungs­einheit ist bei sehr vielen C++-Compilern – so auch bei VC++ – die Datei. Daraus folgt, dass ein C++-Programm also aus einer oder mehreren Dateien besteht, die ge­trennt übersetzt werden.

Natürlich müssen derart getrennt übersetzte Dateien irgendwie zu einem funktionie­renden Ganzen „verbunden“ werden. Dies erledigt der Binder (oder Linker), den Vorgang nennt man binden oder linken. Diese Trennung von Übersetzer und Binder wird von C++ nicht vorgegeben, resultiert aber aus jahrelang üblicher Praxis: Der Binder ist häufig Teil des Betriebssystems und sprachunabhängig, während der Über­setzer vom Anwender zu installieren und sprachabhängig ist.

Kommen wir zurück zu C++: Wir haben geklärt, dass C++-Programme aus einer oder mehreren Übersetzungseinheiten besteht, wissen aber immer noch nicht, woraus eine Übersetzungseinheit besteht.

Im Grunde genommen gilt:

Merksatz 1: Jedes C++-Programm ist eine Folge von Deklarationen!

Jetzt werden Sie sich wahrscheinlich fragen:

(1) Was ist eine Deklaration?

(2) Waren das wirklich alles Deklarationen im vorigen C++-Beispiel?

Zum ersten Punkt: Eine Deklaration verbindet (mindestens) einen Namen und gewis­se Eigenschaften zu einer Entität. Erstes Beispiel: In Zeile 11 des Beispielprogramms heißt es:

11 string name;

Hier wird der Name name mit der Eigenschaft „Variable vom Typ string“ ver­bunden. Man sagt: Die Variable name vom Typ string wird deklariert.

Zweites Beispiel: In Zeile 16 steht:16 void begruesse (string name)

Hier haben wir sogar zwei Deklarationen. Zum einen wird der Name begruesse mit der Eigenschaft „Prozedur mit einem Parameter name vom Typ string“ de­

17

Übersetzungsein­heiten

Binder

Was ist eine De­klaration?

C++-Programme bestehen aus De­klarationen

Page 26: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

klariert. Zum anderen ist die Einführung des Parameters name selbst eine Deklarati­on, denn name wird mit der Eigenschaft „Parameter vom Typ string“ verbunden.

Sie sehen also, dass Deklarationen (fast) überall anzutreffen sind.Falls Sie sich wundern, warum im Skript manchmal von Deklarationen und manchmal von Definitionen die Rede ist: Jede Definition ist auch eine Deklaration, aber nicht um­gekehrt. Infolgedessen sind Definitionen eine „strengere“ Form von Deklarationen. Ge­naueres hierzu finden Sie in Abschnitt 3.3.1.

Zum zweiten Punkt: Nein, nicht alles waren Deklarationen. Zuerst müssen Sie wis­sen, dass die obige Aussage sich auf die „oberste“ Struktur von C++-Programmen bezieht. Damit ist gemeint, dass die Aussage nicht bedeutet, dass innerhalb von De­klarationen (beispielsweise innerhalb von Funktionen) wieder nur Deklarationen an­zutreffen sind. Schauen wir uns das C++-Programm einmal nur auf „oberster“ Ebene an:

1 /*** Beispiel hello.cpp ***/2 #include <istream> // für die Eingabe3 #include <ostream> // für die Ausgabe4 #include <iostream> // für die Objekte cin und cout5 #include <string> // für Zeichenketten-Verarbeitung6 using namespace std;78 string liesName ()9 {

10 // ausgeblendet14 }1516 void begruesse (string name)17 {18 // ausgeblendet19 }2021 int main ()22 {23 // ausgeblendet29 }

(Das ist jetzt kein korrektes C++-Programm mehr, weil die Funktionen keine Werte zurückliefern. Für unsere Zwecke ist dies aber ausreichend; stellen Sie sich einfach vor, die ausgeblendeten Teile würden weiterhin existieren.)

Nun schauen Sie sich das Programm genauer an. In den Zeilen 8-29 stehen drei Funktionen. Da jede davon eine Deklaration darstellt, ist Regel 1 erst einmal erfüllt.

Die Namensraum-Direktive in Zeile 6 ist etwas komplizierter zu verstehen. Sie ha­ben bereits gelernt, dass diese eine Art Abkürzung definiert, um beispielsweise cout statt std::cout schreiben zu können. Sie können sich denken, dass das Objekt cout im Namensbereich std auf irgendeine Weise deklariert ist. Nun stellen Sie sich einfach vor, dass die o. g. Namensraum-Direktive für jede Deklaration im Na­mensraum std eine Deklaration im „aktuellen“ Namensraum einfügt, damit sie auch ohne std-Präfix angesprochen werden kann. So gesehen stellt die Zeile 6 nicht nur eine Deklaration, sondern mehrere, ja sogar ziemlich viele Deklarationen dar. Auf je­den Fall passt Regel 1 immer noch auf das betrachtete Programm.

18

Namensraum-Di­rektiven sind De­klarationen

Page 27: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

Die Zeilen 2 bis 5 hingegen sind definitiv keine Deklarationen, sondern Präprozes­sor-Direktiven. Trotzdem gilt Regel 1. Warum? Einfach aus dem Grund, weil die Re­gel 1 sich auf die C++-Sprachkonzepte bezieht, die der Übersetzer „sieht“ – und der Präprozessor entfernt immer alle Präprozessor-Direktiven, da der Übersetzer damit nichts anfangen kann.

Wozu braucht man dann Präprozessor-Direktiven, wenn sie für den Übersetzer sowieso unsichtbar sind? Nun, der Präprozessor löscht die Direktiven nicht nur, er ersetzt sie mit anderem (Programm-)Text. Man könnte den Präprozessor sozusagen als Text-Prozessor bezeichnen. Im vorliegenden Fall werden die #include-Direktiven durch den Inhalt der angegebenen Dateien ersetzt – und dieser ist für den Übersetzer natürlich wichtig.

Nachdem wir nun also die Gültigkeit der Regel 1 für unser Beispiel-Programm „be­wiesen“ haben, wollen wir uns noch einer wichtigen Eigenart von C++ widmen.

3.1.2 Die main-Funktion

Wenn Sie Erfahrungen mit anderen Programmiersprachen gemacht haben, dann ken­nen Sie sicherlich das Konzept des Hauptprogramms. Das Hauptprogramm ist der Teil des Programms, bei dem die Ausführung des Programms beginnt. In vielen Sprachen wird das Hauptprogramm gar nicht besonders gekennzeichnet, sondern wird implizit von allen Anweisungen gebildet, die außerhalb von Funktionen, Proze­duren und anderen blockartigen Strukturen existieren.

In C++ ist dies aus zweierlei Gründen anders. Zum einen wird das Hauptprogramm deutlich gekennzeichnet: Alle Anweisungen, die in der Funktion mit dem Namen main vorhanden sind, bilden das Hauptprogramm. Daraus folgt unmittelbar, dass je­des C++-Programm irgendwo eine main-Funktion besitzen muss.10 Auch unser Bei­spiel-Programm hat eine main-Funktion.

Zum anderen wird auch hier wieder deutlich, dass trotz der besonderen Bedeutung der main-Funktion diese auch „nur“ eine einfache Funktion ist, mit Namen, Rückga­betyp, (leerer) Parameterliste und so weiter. Das ist typisch für C++: Immer wenn es möglich war, wurde neue Syntax für Sprachelemente vermieden, sondern altbewähr­tes „wiederverwendet“. Es gibt also keine eigene Syntax für die Definition des Hauptprogramms, sondern die existierende Syntax für Funktionen wurde dafür auf­gegriffen. (Das ist gar nicht so abwegig wie es scheint. Immerhin kann das gesamte Programm aus der Sicht des Aufrufers auch als Funktion betrachtet werden.)

Obwohl die main-Funktion wie eine normale Funktion aussieht, ist sie es nicht ganz. Es gibt mehrere Dinge, welche die main-Funktion von anderen Funktionen unterschei­det:

• Sie dürfen sie nicht rekursiv aufrufen (3.6.5).

• Sie dürfen sie nicht static machen.

• Sie dürfen sie nicht inline machen.

• Sie dürfen ihre Adresse (3.4.3.3) nicht erfragen.

10) Wenn Ihre C++-Programme keine main-Funktion benötigen, arbeiten Sie unter Umständen mit einer sogenannten freestanding-C++-Implementierung mit „abgespecktem“ Funktionsumfang. Derartige Implementierungen werden hauptsächlich im Bereich eingebetteter Systeme benutzt und haben andere Mechanismen, um den Beginn des Programms festzulegen.

19

Präprozessor-Di­rektiven gehören nicht zum Kern von C++

das Hauptpro­gramm in C++

main-Funktion bildet den Anfang

main ist einfach eine Funktion

das Besondere an main

Wozu ein Präpro­zessor?

Page 28: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

3.2 Lexikalische ElementeNachdem Sie sich nun mit dem grundlegenden Aufbau von C++-Programmen ver­traut gemacht haben und somit die „größte“ Programmeinheit, die Übersetzungsein­heit, untersucht haben, erfahren Sie in diesem Abschnitt, aus welchen „kleinsten“ Einheiten (Token genannt) ein C++-Programm besteht.

3.2.1 KommentareKommentare dienen der Programm-Dokumentation. Es gibt zweierlei Arten: die ei­nen enden am Ende der Zeile, in welcher der Kommentar steht; die anderen können sich über mehrere Zeilen erstrecken. Die einzeiligen Kommentare beginnen mit //, die mehrzeiligen beginnen mit /* und enden mit */. Beispiel:

1 /*** Beispiel kommentar.cpp ***/2 // Dieser Kommentar endet am Ende der Zeile3 /* Dieser Kommentar beginnt in dieser Zeile...4 ...und endet in dieser Zeile */5 int main () // Das geht auch gemischt mit anderen Elementen: in einer Zeile...6 { /* ...und auch7 mehrzeilig! */ return 0;8 }

Gute Kommentare zu schreiben ist eine Kunst für sich. Generell sollten Sie die Aufga­be, Voraussetzungen und Garantien von Funktionen (3.6), Klassen und Methoden (4.4.5.1) gut dokumentieren. Dies ist wichtig, damit Sie oder andere Nutzer dieser Funktionen wissen, welche Eingaben die Funktion erwartet und welche Ausgabe sie produziert. Auch an Stellen, wo der verwendete Algorithmus nicht offensichtlich oder schwer zu verstehen ist (z. B. bei Rekursion, s. Abschnitt 3.6.5), sollten Kommentare stehen. Schlechte Kommentare sind zum Beispiel:

1) redundante Kommentare, die dasselbe ausdrücken wie der kommentierte Quelltext

2) falsche Kommentare, die das Gegenteil dessen behaupten, was der Quelltext sagt

3) zu kurze oder fehlende Kommentare, die wichtige Informationen unterschlagen

Beispiel:

1 /*** Beispiel fakultaet1.cpp ***/2 #include <ostream>3 #include <iostream>4 using namespace std;56 /*7 * Diese Funktion berechnet die Fakultät.8 * Eingabe:9 * „argument“ – das Argument der Funktion

10 * Ausgabe:11 * das Ergebnis der Fakultät von „argument“12 * Bemerkung:13 * Die Fakultät fakultaet ist rekursiv definiert als:14 * fakultaet(n) = 1 [n = 0]15 * fakultaet(n) = n * fakultaet (n – 1) [n > 0]16 */17 int fakultaet (int argument)18 {19 // pruefe ob argument Null ist20 if (argument == 0)21 // liefere Null zurück

20

einzeilige und mehrzeilige Kom­mentare

die „Atome“ von C++

gute und schlech­te Kommentare

Page 29: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

22 return 1;23 else24 return argument * fakultaet (argument - 1);25 }2627 int main ()28 {29 cout << "5! = " << fakultaet (5) << endl;30 return 0;31 }

Lassen Sie sich nicht abschrecken, wenn Sie nicht alles in diesem Programm-Auszug verstehen. Schauen wir uns in dieser Funktion die Kommentare an:

• Zeile 6-16: Dies ist ein guter Kommentar, weil er alle wichtigen Informationen zum Verständnis dieser Funktion enthält.

• Zeile 19: Dies ist ein schlechter Kommentar, weil er genau das ausdrückt, was der kommentierte Quelltext ohnehin schon tut (Punkt 1).

• Zeile 21: Dies ist ein schlechter Kommentar, da er schlichtweg falsch ist (Punkt 2).

• Zeile 24: Hier fehlt eine Bemerkung zur Rekursion (3.6.5); weil Rekursion für die meisten Menschen nicht einfach zu durchschauen ist, sollte hier ein Kommentar nicht fehlen. (Punkt 3).

Merksatz 2: Kommentiere so gut du kannst!

3.2.2 BezeichnerBezeichner sind Namen von Entitäten in C++. Jede Variable (3.3.2), jede Funktion (3.6), jede Klasse (4.4.5.1) und viele andere Entitäten benötigen Namen. Diese Na­men unterstehen bestimmten Regeln:

(1) Jeder Bezeichner muss mit einem Buchstaben oder einem Unterstrich (_) begin­nen.

(2) Abgesehen vom ersten Zeichen (s. o.) kann ein Bezeichner zusätzlich aus Ziffern bestehen.

(3) Gewisse reservierte Wörter (sogenannte Schlüsselwörter, s. Abschnitt 3.2.3) wie if, return oder int dürfen nicht als Bezeichner verwendet werden.

C++ versteht unter Buchstaben nur die 26 lateinischen Buchstaben (sowohl Groß- als auch Kleinbuchstaben) und keine Umlaute oder andere „Sonderzeichen“ wie Buchsta­ben mit Akzenten o. ä.11 Seien Sie also bei der Vergabe von Namen eher konservativ.

Beispiel: Die Bezeichner name, begruesse, main, test_1, Fakultaet und ___ sind erlaubt12, wohingegen 4gewinnt (beginnt mit Ziffer, Regel 1), Fakul­tät (enthält Umlaut, Regel 2) und new (ist ein Schlüsselwort, Regel 3) nicht erlaubt sind.13

11) Das ist streng genommen falsch: Seit der ISO 14882-C++-Standardisierung ist es erlaubt, viel mehr Zeichen in Bezeichnern zu verwenden. Leider gibt es nur wenige C++-Übersetzer, die dies umsetzen. Insbesondere VC++ implementiert diese Funktionalität nicht.

12) obwohl der letzte natürlich aus Gründen der Verständlichkeit völlig ungeeignet ist13) Das Schlüsselwort new lernen Sie in Abschnitt 4.5.5.1 kennen.

21

Bezeichner be­nennen Entitäten

Regeln für gültige Namen

Page 30: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

Die Groß- und Kleinschreibung ist bei allen Bezeichnern in C++ relevant. Die Bezeich­ner zaehler, Zaehler und ZaEhLeR sind alle unterschiedlich und können in der Tat verschiedene Entitäten benennen. Es ist jedoch davon abzuraten, sehr ähnliche Be­zeichner für unterschiedliche Entitäten zu verwenden, insbesondere Entitäten gleicher Art, die im selben Gültigkeitsbereich (3.3.4) existieren (also z. B. für zwei lokale Varia­blen (3.3.2) in derselben Funktion.)

Ebenso wie gute Kommentare dienen auch gute Bezeichner der besseren Verständlich­keit des Quelltextes. Sie sollten sich angewöhnen, für Ihre Programme Richtlinien fest­zulegen, die Sie dann befolgen. Dies ist insbesondere dann unerlässlich, wenn mehrere Programmierer an demselben Programm arbeiten.

Eine mögliche Sammlung von Richtlinien könnte beispielsweise sein:

• Namen von Funktionen (3.6), Operationen (4.6.1) und Methoden (4.4.5.1) werden klein geschrieben.

• Namen von Typen (3.4) (insbesondere von Klassen (4.4.5.1)) werden mit einem Großbuchstaben begonnen.

• Namen von lokalen Variablen (3.3.2) und Parametern (3.6.4) werden klein ge­schrieben.

• Namen, die aus mehreren Wörtern bestehen, werden durch gemischte Groß- und Kleinschreibung strukturiert (etwa getWeekOfYear). Eine mögliche Alternative ist die Verwendung von Unterstrichen (etwa get_week_of_year).

Wenn Sie bei einem Unternehmen als Programmierer angestellt sind, werden Sie wahr­scheinlich bereits Firmen-eigene Konventionen kennen, die Sie natürlich dann auch be­folgen sollten.

Vielleicht ist Ihnen aufgefallen, dass in diesem Skript die Wörter Bezeichner und Name synonym verwendet werden. Korrekterweise muss erwähnt werden, dass zwischen Ih­nen ein subtiler Unterschied besteht. Von Namen sprechen wir dann, wenn klar ist, wel­che Art von Entität benannt wird, z. B. ist der Bezeichner bei einer Funktionsdefinition der Name der Funktion. Von Bezeichnern sprechen wir dann, wenn der Kontext allge­meiner ist. Beispielsweise werden in Abschnitt 4.4.5.2 qualifizierte Bezeichner vorge­stellt. Ob diese Bezeichner nun Klassen, Funktionen, Variablen etc. benennen, ist nicht bekannt und auch völlig irrelevant. In diesem Fall sprechen wir aber nicht von Namen, weil die benannte Entität unbekannt oder irrelevant ist.

Summa summarum sind also Namen und Bezeichner äquivalent, nur werden beide in unterschiedlichen Kontexten gebraucht, je nachdem welche Informationen zur Verfü­gung stehen.

3.2.3 SchlüsselwörterIn C++ sind einige Wörter als Schlüsselwörter reserviert, so dass sie nicht als Be­zeichner für Variablen, Funktionen, Klassen o. ä. verwendet werden können. Die meisten Schlüsselwörter werden in den übrigen Kapiteln erläutert, hier soll eine voll­ständige Auflistung aller C++-Schlüsselwörter genügen (Tabelle 1), damit Sie diese nicht aus Unkenntnis als Namen verwenden. Schlüsselwörter werden in diesem Skript durchgehend fett gedruckt.

22

gute und schlech­te Bezeichner; Konventionen

Unterschied zwi­schen Namen und Bezeichnern

Schlüsselwörter in C++

Groß- und Klein­schreibung von Bezeichnern

Page 31: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

3.2.4 Operatoren und InterpunktionszeichenC++ kennt eine Vielzahl von Operatoren. Außerdem werden an vielen Stellen Inter­punktionszeichen benötigt, z. B. Kommata zum Trennen von Parametern. Tabelle 2 gibt Ihnen einen Überblick über alle in C++ verwendeten Operatoren und Interpunk­tionszeichen.

Die Token in der letzten Zeile von Tabelle 2 werden von VC++ in der Version 6.0 und auch von einigen anderen C++-Übersetzern nicht erkannt. Deshalb ist anzuraten, sie nicht zu verwenden, zumal es sich nur um Alternativen für andere, besser unterstützte Operator- und Interpunktionszeichen handelt.

Beachten Sie, dass zwischen Operatoren und Interpunktionszeichen nicht streng unter­schieden werden kann, da einige Zeichen je nach Kontext mal Operator und mal Inter­punktionszeichen sind (etwa das Komma). Deshalb bilden sie eine gemeinsame Token-Klasse und nicht zwei.

3.2.5 ZahlenNatürlich kann in C++ auch gerechnet werden, und dabei muss man häufig mit Zah­len-Konstanten hantieren. Ganzzahlen (d. h. Zahlen ohne Nachkommastellen) wer­den in C++ ganz „normal“ in der Dezimalschreibweise dargestellt, wobei – und + als Vorzeichen erlaubt sind. In C++ haben Zahlen gewöhnlich den Typ int (3.4.2.2).

Beispiel: 0, 42, +1789, -1234567

23

Ganzzahlen in C++

Operatoren und Interpunktionszei­chen in C++

asm auto bool breakcase catch char classconst const_cast continue defaultdelete do double dynamic_castelse enum explicit exportextern false float forfriend goto if inlineint long mutable namespacenew operator private protectedpublic register reinterpret_cast returnshort signed sizeof staticstatic_cast struct switch templatethis throw true trytypedef typeid typename unionunsigned using virtual voidvolatile wchar_t while

Tabelle 1: Schlüsselwörter in C++

Page 32: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

Die Vorzeichen + und – sind übrigens nicht Teil der Zahlen, sondern Operatoren, die Sie in Abschnitt 3.4.2.2 kennen lernen werden. Dies hat insofern Auswirkungen auf die Zahlen, als dass Sie nicht unmittelbar vor der jeweiligen Zahl stehen müssen, sondern durchaus durch Freiraum getrennt sein können. - 4 ist also genauso korrekt wie -4.

Nicht alle Zahlen müssen den Typ int haben. Wenn eine Zahl im Quelltext zu groß für den Typ int aber klein genug für den Typ long ist, ist die Zahl vom Typ long. Wenn er selbst für long zu groß ist, bekommt die Zahl den Typ unsigned long zugeordnet. Sollte die Zahl jedoch sogar für den Typ unsigned long zu groß sein, liegt ein Fehler vor, und der Übersetzer bricht die Verarbeitung des C++-Programms ab.

Sie können auch den Typ von Zahlen-Konstanten explizit angeben, indem Sie die Kon­stante mit einem Suffix versehen: 2u (Typ unsigned int), -30000L (Typ long), 12345uL (Typ unsigned long). Bei dem Suffix ist Groß- und Kleinschreibung

24

Vorzeichen

Datentyp von Zahlen-Konstan­ten

Operator Bedeutung{ } Blockanfang und -ende[ ] Indizierung von Feldern# ## Präprozessor-Operatoren; Abschluss von Anweisungen und Deklarationen:: Operator zum Auflösen von Gültigkeitsbereichen( ) Klammern zum Regeln der Priorität;

Operator zum Aufrufen von Funktionen+ - * / % arithmetische Operatoren! && || logische Operatoren== != < > <= >= relationale Operatoren^ & | ~ bitweise Operatoren<< >> Bitschiebeoperatoren. -> .* ->* Operatoren zum Zugriff auf Klassen-Elemente++ -- Operatoren zum Inkrementieren und Dekrementieren? Operator zur Fallunterscheidung= += -= *= /= %= ^= &= |= <<= >>=

Zuweisungsoperatoren

, : ... Komma-Operator, Interpunktionszeichen<: :> <% %>%: %:%:not and or xorcomplbitand bitornot_eq and_eqor_eq xor_eq

alternative Token

Tabelle 2: Operatoren und Interpunktionszeichen in C++

Page 33: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

möglich, allerdings sollten Sie das kleine L (‘l’) wegen der Ähnlichkeit zur Eins (‘1’) nach Möglichkeit nicht verwenden.

Zahlen mit Nachkommastellen sind in C++ auch möglich, jedoch nur als Fließkom­mazahlen (s. u.) Der Dezimalpunkt (.) wird verwendet, um die Vorkomma- und die Nachkommastellen voneinander zu trennen (obwohl sie Fließkommazahlen heißen!) Sie können auch die Exponential-Schreibweise verwenden, die im wissenschaftli­chen Umfeld häufig verwendet wird. Sie müssen nur aufpassen, dass entweder der Dezimalpunkt oder der Exponent vorhanden ist, damit die Zahl als Fließkommazahl und nicht als Ganzzahl aufgefasst wird. Schließlich sind Vorzeichen genauso wie bei den ganzen Zahlen erlaubt. Fließkommazahlen haben in C++ normalerweise den Typ double (3.4.2.3).

Beispiel: 1.7, -2.0, +77E4 (entspricht 77×104), 0.00125, 1.25E-3 (entspricht 1,25×10-3 und ist folglich mit der vorletzten Zahl identisch)

Fließkommazahlen heißen so, weil die Anzahl der Stellen nach dem Komma davon ab­hängt, wie viele Stellen vor dem Komma bereits „verbraucht“ sind. Nehmen Sie einmal an, dass für Fließkommazahlen 18 Stellen zur Speicherung von Ziffern zur Verfügung stellen. Wenn Sie vor dem Komma nur eine Ziffer haben, stehen Ihnen nach dem Kom­ma bis zu 17 Stellen zur Verfügung. Besitzt die Zahl jedoch einen großen Vorkomma-Anteil mit 17 Ziffern, steht Ihnen dann nur noch eine Stelle hinter dem Komma zur Ver­fügung. Das Komma „fließt“ also innerhalb der Zahl hin und her und hat keinen festen Platz. Dies ist bei den sogenannten Festkommazahlen anders, diese werden jedoch von C++ nicht direkt unterstützt.

Auch bei Fließkommazahlen gibt es Suffixe, die den Konstanten angehängt werden können, um den standardmäßig verwendeten Typ double zu ändern. Wird ein f ange­hängt (z. B. 0.1f), ist die Fließkommazahl vom Typ float, bei einem L (z. B. 1.123456789L) ist sie vom Typ long double.

3.2.6 Zeichen und ZeichenkettenNoch häufiger als Zahlen spielen bei der Programmierung Zeichen und Zeichenket­ten eine Rolle. In C++ werden Zeichen-Konstanten in einfachen Anführungsstrichen ('), Zeichenketten-Konstanten in doppelten (") eingeschlossen.

Beispiele für Zeichen-Konstanten: 'x', 'A', ' 'Beispiele für Zeichenketten-Konstanten: "Hallo", "C++ ist toll", ""Wie Sie sehen, gibt es in C++ sehr wohl die leere Zeichenkette, aber nicht das „leere“ Zeichen.14

Zeichen-Konstanten sind generell vom Typ char. Bei Zeichenketten ist das etwas schwieriger, denn Zeichenketten sind nicht automatisch vom Typ string. Dies liegt daran, dass string kein in C++ eingebauter Datentyp ist, sondern von der C++-Stan­dard-Bibliothek bereitgestellt wird. Wie Sie mit Zeichenketten am besten umgehen, er­fahren Sie in Abschnitt 3.4.2.1.

Welche Zeichen in Zeichen- und Zeichenketten-Konstanten vorkommen dürfen, er­fahren Sie im Detail in Abschnitt 3.4.2.1. Hier soll erstmal der Hinweis genügen,

14) Dies macht Sinn, wenn Sie Zeichenketten als (endliche) Folgen aus dem Alphabet verfügbarer Zeichen ansehen, entsprechend dem Kleene-Stern-Operator (*) in regulären Ausdrücken.

25

Fließkommazah­len in C++

Begrenzungen von Zeichen und Zeichenketten

Fließkommazah­len und Festkom­mazahlen

Typ von Zeichen und Zeichenket­ten

Page 34: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

dass alle Zeichen der C++-Syntax (also Buchstaben, Ziffern, Operatoren etc.) auch in Zeichen-und Zeichenketten-Konstanten vorkommen dürfen.

Wenn Sie Zeichen-Konstanten ein großes L voranstellen (z. B. L'x'), sind diese vom Typ wchar_t und können Zeichen aus einem wesentlich größeren Zeichenbereich ent­halten. Ähnliches gilt für Zeichenketten-Konstanten. Näheres hierzu können Sie im Ab­schnitt 3.4.2.1 nachlesen.

In jeder Programmiersprache gibt es das Problem, dass die Begrenzungszeichen für Zeichen und Zeichenketten (also einfache und doppelte Anführungsstriche in C++) innerhalb jener nicht vorkommen dürfen. Um diese jedoch trotzdem verwenden zu können, gibt es in C++ eine besondere Schreibweise: Um einfache Anführungsstri­che in Zeichen-Konstanten und doppelte Anführungsstriche in Zeichenketten-Kon­stanten benutzen zu können, muss ihnen ein Backslash oder umgekehrter Schräg­strich (\) als Fluchtsymbol vorangestellt werden. Dadurch werden diese Zeichen maskiert, so dass sie vom Übersetzer nicht als Zeichen- oder Zeichenketten-Begren­zungen verstanden werden.

Beispiel: '\'' , "eine \"echte\" Verbesserung"Neben der Maskierung von Begrenzungszeichen hat der Backslash auch noch eine andere Funktion: die Notation von Steuerzeichen. Beispielsweise sind Zeilenumbrü­che innerhalb von Zeichen- und Zeichenketten-Konstanten nicht erlaubt. Um den­noch ein solches Zeichen zu verwenden, verwenden Sie das Steuerzeichen \n. (Das „n“ steht für „new line“ und bezeichnet den Beginn einer neuen Zeile.)

Beispiel: "Erste Zeile.\nZweite Zeile."Steuerzeichen werden so genannt, weil sie keine Zeichen-Repräsentation besitzen und somit nicht dargestellt werden können. Vielmehr haben sie Auswirkungen auf die nach­folgende Ausgabe und „steuern“ sie.

Weiterhin macht es die Sonderfunktion des Backslash notwendig, ihn selbst zu mas­kieren, falls er als Zeichen vorkommen soll. Das heißt im Klartext, dass jeder umge­kehrte Schrägstrich in der gewünschten Zeichenkette durch zwei umgekehrte Schräg­striche im Quelltext dargestellt werden muss.

Beispiel: "Vier umgekehrte Schrägstriche: \\\\\\\\"Der Backslash wird als Fluchtsymbol bezeichnet, da er der normalen Zeichen-Verarbei­tung „entflieht“ und eine besondere Verarbeitung des nachfolgenden Zeichens aktiviert. Drei der möglichen Verwendungen haben Sie bereits kennen gelernt: die Maskierung von Begrenzungszeichen, die Maskierung des Fluchtsymbols selbst und die Einführung von Steuerzeichen. Neben weiteren Steuerzeichen existieren noch weitere Modi, die je­doch eher esoterisch sind und in der täglichen Programmierung nicht benötigt werden.

Zum Schluss sollten Sie wissen, dass Zeichenketten-Konstanten, die im Quelltext ne­beneinander geschrieben werden, automatisch aneinander gehängt werden. Dies ist vor allem dann sinnvoll, wenn besonders lange Zeichenketten im Quelltext darge­stellt werden.

Beispiel:1 /*** Beispiel zeichenketten1.cpp ***/2 #include <ostream>

26

Maskierung von Begrenzungszei­chen

Steuerzeichen

Backslash inner­halb von Kon­stanten

Zeichenketten werden aneinan­dergehängt

Page 35: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

3 #include <iostream>4 using namespace std;56 int main ()7 {8 cout << "Hal"9 "lo" " Welt!" << endl;10 return 0;11 }

ist äquivalent zu:1 /*** Beispiel zeichenketten2.cpp ***/2 #include <ostream>3 #include <iostream>4 using namespace std;56 int main ()7 {8 cout << "Hallo Welt!" << endl;9 return 0;10 }

3.2.7 TrennerEgal wie diese „kleinsten“ Einheiten aussehen, der Übersetzer muss sie voneinander unterscheiden können. In vielen Fällen gelingt es ihm nur, wenn zwischen ihnen et­was Freiraum (oder Whitespace) existiert. Zu diesem Freiraum gehören Leerzeichen, Tabulator-Zeichen und Zeilenumbrüche.

Nicht immer wird solcher Freiraum benötigt. Generell gilt, dass Freiraum immer dann benötigt wird, wenn ohne Freiraum etwas syntaktisch Gültiges herauskommen könnte.

Beispiel:1 intmain () // falsch: intmain ist ein gültiger Bezeichner, Freiraum zur Trennung notwendig2 {3 int ergebnis=42; // zwischen = und 42 ist kein Freiraum notwendig,4 // da "=42" als Ganzes in der C++-Syntax nicht existiert5 returnergebnis; // falsch, returnergebnis ist gültiger Bezeichner6 }

Da die Regeln insbesondere für Einsteiger nicht unbedingt offensichtlich sind, tren­nen Sie nach Möglichkeit zwei Token immer mit einem Leerzeichen o. ä. voneinan­der. Moderne Entwicklungsumgebungen können Sie jedoch in den fraglichen Fällen durch Hervorhebung von Schlüsselwörtern, Bezeichnern, Zahlen, Zeichenketten u. ä. durch spezielle Farben, Schriftarten und -effekte unterstützen.

So formatiert VC++ im obigen Beispiel int in intmain nicht farbig, so dass Sie un­mittelbar erkennen können, dass int und main nicht als unterschiedliche Token vom Übersetzer erkannt werden.

3.3 Namen und EntitätenDieser Abschnitt behandelt so ziemlich alles, was Sie über Namen und benannte En­titäten in C++ wissen sollten. Unter Entitäten wird im Folgenden alles verstanden, was einen Namen hat. Sie kennen bereits Variablen (3.3.2), Funktionen (3.6) und Ty­

27

Trennung von To­ken

besser zuviel als zuwenig trennen

Page 36: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

pen (3.4). Später werden Sie noch andere „Gebilde“ kennen lernen, die ebenfalls En­titäten darstellen, u. a. Aufzählungen (3.4.3.6), Namensräume (8.2) und Schablonen (7.2). Ihnen allen gemeinsam ist, dass sie über eine Deklaration oder Definition einen Namen zugeordnet bekommen, über den sie angesprochen werden können.

3.3.1 Deklarationen, Definitionen und Header-DateienJedes Objekt muss in C++ vor der Benutzung deklariert werden. Durch eine Dekla­ration werden der Name und bestimmte Eigenschaften der Entität wie sein Typ mit­einander verbunden. Eine Definition geht noch einen Schritt weiter und erzeugt die betrachtete Entität auch gleich. Während also eine Deklaration dem Übersetzer nur mitteilt „es existiert eine Entität mit diesem Namen und diesem Typ“, sagt eine Defi­nition „erzeuge eine Entität mit diesem Namen und diesem Typ“. Da das Erstellen die Existenz der Entität quasi beinhaltet, ist jede Definition auch eine Deklaration, während der umgekehrte Fall nicht gilt.

Deklarationen sind also als Verweise zu verstehen, während Definitionen die erzeu­genden Konstrukte sind. Daraus folgt, dass es mehrere Deklarationen derselben Enti­tät geben kann aber nur eine (und genau eine!) Definition der Entität existiert. Die letzte Erkenntnis wird in C++ die One Definition Rule (Eine-Definition-Regel) ge­nannt:

Merksatz 3: Für jede verwendete Entität gibt es genau eine Definition!

Deklarationen sind innerhalb des Programms nur in einem bestimmten Bereich, dem sogenannten Gültigkeitsbereich, sichtbar. Auf den Gültigkeitsbereich geht der Ab­schnitt 3.3.4 genauer ein.

Eine Deklaration ist in C++ immer folgendermaßen aufgebaut:

Typ Deklarator [= Ausdruck];Deklarationen sind in C++ immer typisiert, ein Typ (3.4) muss immer angegeben werden. Danach folgt der Deklarator, der im einfachsten Fall (und nur der ist mo­mentan interessant) lediglich aus einem Bezeichner besteht. Optional kann dann ein Ausdruck (3.4) als Initialwert folgen; dies macht aber nur dann Sinn, wenn eine Va­riable oder Konstante definiert wird. (Der Initialwert ist somit immer Teil von Defi­nitionen, niemals von „reinen“ Deklarationen.) Mehr zum Initialisieren von Varia­blen (oder Konstanten) finden Sie in Abschnitt 3.3.3.

Funktionen werden nicht in diesem Sinne initialisiert, vielmehr werden zwischen zwei geschweifte Klammern die zugehörigen Anweisungen niedergeschrieben. Bei­spiele hierfür haben Sie bereits mehrfach im Skript gesehen. Auf Funktionen und ihre Besonderheiten geht Abschnitt 3.6 ein.

Zu diesem grundlegenden Aufbau von Deklarationen gibt es viele Ausnahmen. So kann der Deklarator beispielsweise gänzlich fehlen, wenn ein Typ (etwa eine Klasse) defi­niert wird. In seltenen Fällen wird der Typ weggelassen, wenn er durch den Kontext un­missverständlich ist, beispielsweise bei Konstruktoren (4.5.1). Der Initialwert macht hingegen nur bei Variablen oder Konstanten Sinn, bei Funktionen ist er in dieser Art und Weise fehl am Platz. Schließlich gibt es in C++ noch eine andere Form der Initiali­

28

Deklarationen und Definitionen im Vergleich

die ODR (Eine-Definition-Regel)

Aufbau von De­klarationen und Definitionen

Gültigkeit von Deklarationen

Page 37: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

sierung, die aber erst verständlich wird, wenn Klassen und Konstruktoren behandelt werden (4.5).

Grau ist alle Theorie! Betrachten wir das folgende Beispiel:15

1 /*** Beispiel antwort1.cpp ***/2 #include <ostream>3 #include <iostream>4 using namespace std;56 int antwortAufDasLebenDasUniversumUndAlles ()7 {8 return 42;9 }10 int main ()11 {12 int antwort = antwortAufDasLebenDasUniversumUndAlles ();13 cout << "Die Antwort ist " << antwort << endl;14 return 0;15 }

In diesem Beispiel liegen drei Deklarationen vor, die gleichzeitig Definitionen sind:

• Zeile 6: Hier wird die Funktion antwortAufDasLebenDasUniversum­UndAlles definiert. Eine Definition liegt vor, weil nicht nur Name, (nicht vor­handene) Parameter und Rückgabetyp spezifiziert werden, sondern auch der „In­halt“, sprich die enthaltenen Anweisungen (Zeile 7 bis 9).

• Zeile 10: dito, aber auf Funktion main bezogen

• Zeile 12: Hier wird die Variable antwort vom Typ int definiert. Variablen-Deklarationen sind fast immer Definitionen, insbesondere dann, wenn sie – wie auch in diesem Fall – initialisiert werden. Der Initialwert ist in dem Beispiel kei­ne Konstante, sondern ein (zum Typ der Variable) passender Ausdruck (3.4).

Wie Sie sehen, waren in diesem Beispiel alle Deklarationen auch Definitionen. Viel­leicht fragen Sie sich dann, wann denn „reine“ Deklarationen verwendet werden. Nun, Sie erinnern sich, dass jede Entität vor der Benutzung deklariert sein muss. Nicht immer wollen Sie jedoch die Entität dann auch definieren, weil sie bereits defi­niert ist, nur an einer anderen Stelle. Schreiben wir das obige Beispiel einmal so um, dass die Funktion antwortAufDasLebenDasUniversumUndAlles im Quell­text hinter der Funktion main steht:

1 /*** Beispiel antwort2.cpp ***/2 #include <ostream>3 #include <iostream>4 using namespace std;56 int antwortAufDasLebenDasUniversumUndAlles ();7 int main ()8 {9 int antwort = antwortAufDasLebenDasUniversumUndAlles ();10 cout << "Die Antwort ist " << antwort << endl;11 return 0;12 }13 int antwortAufDasLebenDasUniversumUndAlles ()14 {

15) Douglas Adams lässt grüßen!

29

Vorwärtsreferen­zen

Page 38: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

15 return 42;16 }

Jetzt sieht die Sache schon etwas anders aus. Um in Zeile 9 die Funktion ant­wortAufDasLebenDasUniversumUndAlles nutzen zu können, muss deren Existenz vorher dem Übersetzer mitgeteilt worden sein. Das geschieht in Zeile 6, in der die Funktion deklariert, aber nicht definiert wird. Die Definition finden Sie in den Zeilen 13-16.

Java-Programmierer sollten sich die Unterschiede zu Java klar machen: In C++ müssen Sie jeden Bezeichner vor seiner Verwendung in einer Übersetzungseinheit auf irgendei­ne Weise deklarieren.16 In Java ist dies bei globalen Entitäten (Klassen, Methoden und Attribute) nicht der Fall. Deshalb gibt es in Java auch keine reinen Deklarationen, denn sie sind nicht notwendig.

Natürlich kann man die erste Version des Beispiels verwenden und somit die Dekla­ration in diesem Fall einsparen, aber das geht nicht immer. Manchmal kommt man um Vorwärtsreferenzen nicht herum, insbesondere dann nicht, wenn sich zwei Funk­tionen gegenseitig rekursiv aufrufen (wechselseitige Rekursion, s. Abschnitt 3.6.5).

Solche Deklarationen finden Sie aber nicht nur dann vor, wenn Sie Vorwärtsbezüge in Ihrem Quelltext benötigen. Jedes Mal, wenn Sie Funktionen, Methoden, Variablen oder andere Entitäten auf mehrere Dateien aufteilen und somit in einer Datei definie­ren und in einer anderen Datei benutzen wollen, werden Sie gezwungen sein, Dekla­rationen zu verwenden. Dies geschieht in der Regel, indem die Deklarationen in ei­ner sogenannten Header-Datei oder kurz im Header zusammengefasst werden, die in den benutzenden Dateien eingebunden wird. Die Header-Datei enthält dann die Schnittstelle der zu benutzenden Datei. Übrigens enden Header-Dateien oft mit der Erweiterung h oder hpp, um sie von anderen Quellen unterscheiden zu können.17 Dateien mit der Implementierung haben üblicherweise die Erweiterung cpp, cc oder cxx.

C++ besitzt keine Sprachkonstrukte, um Module oder Pakete, wie sie aus anderen Pro­grammiersprachen wie Java, Pascal oder Oberon bekannt sind, abzubilden. Die Header-Datei(en) und die implementierende(n) Quellen-Datei(en) können jedoch als öffentli­cher und privater Teil eines Moduls angesehen werden. Sie müssen jedoch selbst darauf achten, dass die Deklarationen in der Header-Datei und die Definitionen in der Quell-Datei übereinstimmen. Am einfachsten geht dies, indem Sie in den implementierenden Quellen die Header-Dateien ebenfalls einbinden. Dann wird der Übersetzer (häufig, aber nicht immer!) meckern, wenn die Deklarationen und Definitionen sich unterschei­den.

Um Ihnen an dieser Stelle einen Einblick in die Benutzung von Header-Dateien zu geben, wollen wir das obige Beispiel etwas umschreiben. Wir werden die Funktion antwortAufDasLebenDasUniversumUndAlles in eine eigene Datei hinein­tun. Da wir die Funktion aus dem Hauptprogramm heraus aufrufen müssen, brauchen

16) Es gibt nur sehr wenige Ausnahmen, etwa die Verwendung von Bezeichnern in inline-Metho­den einer Klassendefinition.

17) Die Header-Dateien der C++-Standard-Bibliothek (beispielsweise iostream und string, die Sie bereits im ersten Beispiel kennen gelernt haben) bilden hier eine Ausnahme. Allerdings liegt dies weniger an der Überzeugung, dass eine Endung nicht notwendig ist, sondern eher daran, dass das C++-Standardisierungsgremium sich nicht auf eine einheitliche Endung einigen konnte...

30

Header-Dateien definieren die Schnittstelle

C++ und Module

Header in der C++-Praxis

Page 39: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

wir eine Header-Datei, welche die Schnittstelle der Funktion entsprechend deklariert. Wir erhalten also die folgenden drei Dateien:

• antwort.h:1 /*** Beispiel antwort3/antwort.h ***/2 /***********************************3 * Schnittstelle des Moduls "Antwort"4 ***********************************/5 /*6 * Ermittelt die Antwort auf das Leben, das Universum und Alles.7 * Eingabe: --8 * Ausgabe: Die ANTWORT9 */10 int antwortAufDasLebenDasUniversumUndAlles ();

• antwort.cpp:1 /*** Beispiel antwort3/antwort.cpp ***/2 #include "antwort.h" // Schnittstelle des Moduls "Antwort" einbinden3 int antwortAufDasLebenDasUniversumUndAlles ()4 {5 // siehe Adams, Douglas: "Per Anhalter durch die Galaxis"6 return 42;7 }

• main.cpp:1 /*** Beispiel antwort3/main.cpp ***/2 #include "antwort.h" // Schnittstelle des Moduls "Antwort" einbinden3 #include <ostream>4 #include <iostream>5 using namespace std;67 int main ()8 {9 int antwort = antwortAufDasLebenDasUniversumUndAlles ();10 cout << "Die Antwort ist " << antwort << endl;11 return 0;12 }

Wie Sie sehen, wurde im Beispiel auch ordentlich kommentiert. Dies ist besonders beim Definieren von Schnittstellen wichtig, damit der Nutzer dieser Schnittstelle spä­ter weiß, welche Funktion, welche Klasse usw. welche Aufgabe hat. Behalten Sie immer Merksatz 2 über Kommentare in Abschnitt 3.2.1 im Gedächtnis! Und weil es gerade so schön ist, kommt gleich ein weiterer Merksatz hinzu:

Merksatz 4: Tue Schnittstellen und Implementierung in verschiedene Dateien!

Es gibt zwei Möglichkeiten, Header-Dateien über die #include-Direktive einzubin­den. Der Dateiname der Header-Datei kann entweder innerhalb spitzer Klammern oder innerhalb von doppelten Anführungsstrichen stehen.

Der Unterschied liegt darin, wo der Präprozessor nach der einzufügenden Header-Datei sucht. Wenn der Name in Anführungsstrichen gesetzt ist, sucht der Präprozessor nach der einzufügenden Datei relativ zum Verzeichnis der Datei, welche die #include-Di­rektive enthält. Die Suche in diesem Verzeichnis fällt weg, wenn der Name in spitzen Klammern steht. Die erste Schreibweise wird deshalb häufig für Schnittstellen von (ex­ternen) Bibliotheken verwendet, die zweite für Header-Dateien, die direkt zum Pro­

31

Spitze Klammern oder Anführungs­striche?

Aufteilung von Schnittstelle und Implementierung

Page 40: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

gramm gehören. Denn letztere liegen fast immer im selben Verzeichnis oder zumindest in Verzeichnissen, die nahe beieinander liegen.

Abbildung 13 zeigt, wie die Deklarationen und Definitionen sowie die Verwendung der jeweiligen Namen im letzten Beispiel in Verbindung stehen. Die Header-Datei ist dabei nicht abgebildet, weil sie durch den Präprozessor in die Übersetzungseinheiten eingefügt und deren Bestandteil wird.

3.3.2 VariablenVariablen sind veränderbare Speicherbereiche für Daten. In einer Variable halten Sie Informationen fest, die Sie während des Programmlaufs erhalten oder berechnen. Va­riablen können mit Ausnahme von Funktionstypen (3.4.3.1) und const-modifizier­ten Typen (3.4.4) jeden beliebigen Typ besitzen. Die Form einer Variablen-Definiti­on haben Sie bereits im letzten Abschnitt kennen gelernt.

3.3.3 InitialisierungJede Variable, die Sie definieren, sollte explizit mit einem Ausdruck initialisiert wer­den. Dies ist in C++ besonders wichtig, da C++ andernfalls nicht garantiert, dass in solchen nicht initialisierten Variablen ein sinnvoller Standard-Wert steht.

Dies unterscheidet C++ von anderen Programmiersprachen wie Java, in denen nicht in­itialisierte Variablen auf Null oder einen ähnlichen Wert gesetzt werden. Verlassen Sie sich niemals auf den Inhalt von Variablen, die Sie nicht initialisiert haben oder denen Sie im Verlauf des Programms keinen Wert zugewiesen (3.4.1) haben! Im ungünstigs­ten Fall kann es bereits beim Lesen des Inhaltes einer nicht initialisierten Variable zu ei­nem Programmfehler (sprich Programmabsturz) kommen!

32

Variablen erfor­dern einen Initial­wert

Abbildung 13: Verteilte Deklarationen und Definitionen

Übersetzungseinheit antwort.cpp

Definition der Funktion antwortAufDasLebenDasUniversumUndAlles

Beziehung hergestellt durch ÜbersetzerBeziehung hergestellt durch Binder

Legende:

Übersetzungseinheit main.cpp

Deklaration der Funktion antwortAufDasLebenDasUniversumUndAlles

Definition der Funktion main

Verwendung der Funktion antwortAufDasLebenDasUniversumUndAlles

Page 41: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

Merksatz 5: Verwende niemals nicht initialisierte Variablen!

Es gibt zwei Möglichkeiten, den Initialwert anzugeben: die funktionale Schreibweise und diejenige mit einem Gleichheitszeichen. In den Fällen, in denen beide erlaubt sind, sind beide äquivalent. Beispielsweise haben

1 int antwort = 42;und

1 int antwort (42);dieselbe Bedeutung: Es wird eine Variable namens antwort definiert und mit dem Wert 42 belegt.

Die zweite Form der Initialisierung neigt jedoch manchmal zu Mehrdeutigkeiten mit anderen C++-Sprachkonstrukten und sollte lediglich für die Initialisierung von ob­jektwertigen Variablen (4.5.1) verwendet werden. (Falls ein Objekt mehrere Argu­mente zur Initialisierung benötigt, ist letztere Syntax auch die einzige Möglichkeit, das Objekt angemessen zu initialisieren.)

Klammern gehören in C++ zu den am häufigsten verwendeten syntaktischen Elementen, deswegen sind Notationen zu bevorzugen, die weniger auf Klammern und mehr auf an­dere syntaktische Elemente (etwa das Gleichheitszeichen) setzen. Dies verbessert häufig auch die Verständlichkeit der Fehlermeldungen, wenn Syntax-Fehler im Quelltext er­kannt werden.

Generell gilt, dass der Typ des initialisierenden Ausdrucks mit dem Typ der zu initia­lisierenden Entität übereinstimmen oder zumindest verträglich sein muss. Die Ver­träglichkeit von Typen wird in Abschnitt 3.4.5 diskutiert.

3.3.4 GültigkeitsbereicheWenn Sie etwas deklarieren (sei es eine Variable, eine Funktion oder ein Typ), so verbinden Sie nicht nur einen Namen mit gewissen Attributen wie dem Typ (3.4), sondern machen den Namen auch in einem bestimmten Bereich des Programms ver­fügbar. Dieser Bereich nennt sich Gültigkeitsbereich und stellt den maximalen Be­reich dar, innerhalb dessen die deklarierte Entität verwendet werden kann. Allgemein gilt, dass der Gültigkeitsbereich eines deklarierten Namens von dessen Deklaration bis zum Ende des jeweiligen Blocks (3.5.6) reicht.

Abbildung 14 versucht, den Begriff des Gültigkeitsbereichs anhand des C++-Pro­gramms aus Kapitel 2 zu veranschaulichen.

• Zeile 8: Die hier deklarierte Funktion liesName ist laut Regel bis zum Ende des enthaltenden „Blocks“ gültig. Da die Funktion sich in keinem Block befin­det, ist sie bis zum Ende der Übersetzungseinheit gültig.

• Zeile 11: Die Variable name wird hier innerhalb der Funktion liesName de­klariert. Der Gültigkeitsbereich erstreckt sich bis zum Ende des enthaltenden Blocks, hier also bis zum Ende der Funktion. Außerhalb der Funktion ist die Va­riable nicht verfügbar.

33

zwei Formen der Initialisierung

Typen müssen verträglich sein

Namen sind nur begrenzt gültig

Page 42: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

• Zeile 16: Hier stehen gleich zwei Deklarationen. Zum einen wird die Funktion begruesse deklariert, die ab diesem Punkt bis zum Ende der Übersetzungsein­heit gültig ist. Zum anderen wird der Parameter name deklariert, der nur inner­halb der Funktion Gültigkeit hat.

• Zeile 21: Die an dieser Stelle deklarierte Funktion main ist bis zum Ende der Übersetzungseinheit gültig.

• Zeile 25: Hier wird eine Variable namens meinName deklariert. Sie steht inner­halb der Funktion main, somit ist sie maximal bis zum Ende dieser Funktion gültig.

Beachten Sie an dieser Stelle, dass die Variable name innerhalb der Funktion lies­Name und der Parameter name innerhalb der Funktion begruesse nichts, aber auch gar nichts miteinander zu tun haben und sozusagen nur zufällig gleich heißen (und vom selben Typ sind). Die Gültigkeitsbereiche beider Variablen sind disjunkt (d. h. sie über­lappen sich nicht), somit sind sie auch immer zu unterschiedlichen Zeitpunkten gültig

34

Abbildung 14: Gültigkeitsbereiche

1234567891011121314151617181920212223242526

liesName

name

begruesse name

main

meinName

using namespace std;

string liesName (){ cout << "..."; string name; cin >> name; return name;}

void begruesse (string name)

#include <iostream>#include <string>

}

int main (){ cout << "..." << endl;

string meinName = liesName(); begruesse (meinName);

return 0;}

cout << "..." << endl;{

#include <ostream>#include <istream>/*** Beispiel hello.cpp ***/

272829

Page 43: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

und verwendbar, und Werte in der einen Variablen beeinflussen nicht im Geringsten den Inhalt der anderen Variable (und umgekehrt).

An dieser Stelle ist ein kleiner Überblick über die verschiedenen Arten von Gültig­keitsbereichen in C++ angebracht:

(1) Lokaler oder Block-Gültigkeitsbereich:Namen, die innerhalb von Funktionen oder Methoden deklariert werden, sind lo­kal zu dieser Funktion und erstrecken sich bis zum Ende des sie enthaltenden Blocks. Beispiele umfassen u. a. die Variablen name und meinName der Funk­tionen liesName bzw. main sowie den Parameter name der Funktion be­gruesse.

(2) Globaler Gültigkeitsbereich oder Namensraum-Gültigkeitsbereich:Namen, die außerhalb aller Funktionen, Methoden und Typen deklariert sind, sind global oder Teil eines sogenannten benannten Namensraumes (8.2). Deren Gültigkeit erstreckt sich bis zum Ende der Übersetzungseinheit bzw. bis zum Ende des Namensraums. Beispielsweise gehören alle drei Funktionsdefinitionen aus dem obigen Beispiel zu diesem Gültigkeitsbereich.

(3) Funktionsprototyp-Gültigkeitsbereich:Parameter, die innerhalb von „reinen“ Funktionsdeklarationen eingeführt wer­den, sind nur innerhalb dieser Deklaration gültig. In unserem Beispiel kommt dieser Gültigkeitsbereich nicht vor; hätte aber die Deklaration der Funktion antwortAufDasLebenDasUniversumUndAlles im Abschnitt 3.3.1, „Vorwärtsreferenzen“ einen Parameter, wäre dieser nur in diesem Bereich gültig.

(4) Klassen-Gültigkeitsbereich:Namen, die innerhalb von Klassendefinitionen (4.4.5.2) deklariert werden, sind überall im Kontext dieser Klasse gültig; zu diesem Kontext gehören beispiels­weise die Klassendefinition selbst sowie alle zugehörigen Methoden-Definitio­nen. Klassen lernen Sie in Kapitel 4 kennen.

3.3.5 SichtbarkeitWie anfangs erwähnt ist der Gültigkeitsbereich der maximale Bereich, innerhalb des­sen ein Name verwendet werden kann. Es kann aber passieren, dass eine Deklaration von einer anderen mit demselben Namen verdeckt wird. In einem solchen Fall ist ein Name kurzzeitig unsichtbar (obwohl gültig), und zu einem späteren Zeitpunkt kann er wieder sichtbar und normal verwendet werden.

Betrachten wir dabei folgendes Beispiel:1 /*** Beispiel sichtbarkeit.cpp ***/2 #include <ostream>3 #include <iostream>4 #include <string>5 using namespace std;67 string begruessung (string name)8 {

35

Arten von Gültig­keitsbereichen

sichtbar oder ver­deckt?

Page 44: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

9 string hallo = "Hallo ";10 string begruessung = hallo + name; // bzgl. "+" s. Abschnitt 3.4.2.111 return begruessung;12 }1314 int main ()15 {16 cout << begruessung ("Welt") << endl;17 return 0;18 }

Betrachten Sie einmal die Funktion begruessung, die in den Zeilen 7-12 definiert ist, insbesondere die Zeile 10. Hier wird eine (lokale) Variable namens begrues­sung definiert, deren Name derselbe ist wie der von der Funktion. Ab diesem Punkt kann die Funktion begruessung nicht mehr über ihren Namen angesprochen wer­den, weil der Name nun für eine lokale Variable vergeben ist. Man spricht davon, dass die Variable die Funktion verdeckt hat. Die Funktion ist kurzzeitig nicht sicht­bar geworden (Abbildung 15, blau schraffierter Kasten).

Allerdings währt dieses Verdecken nicht ewig – am Ende des Gültigkeitsbereichs der Variable begruessung (in Zeile 12) kann die Funktion wieder über Ihren Namen erreicht werden, so wie in Zeile 16. Da der Gültigkeitsbereich der Variable be­gruessung nicht in die Funktion main hineinreicht, „weiß“ sie nichts davon, und so spielt auch das Verdecken hier keine Rolle. Die Funktion ist hier ganz normal sichtbar.

Das Verdecken von Namen taucht immer dann auf, wenn mehrere Gültigkeitsberei­che „aufeinander prallen“. Dies passiert insbesondere häufig bei der objektorientier­ten Programmierung, wo jede Klasse einen eigenen Gültigkeitsbereich bildet und

36

Verdecken in der Praxis

Verdecken ist be­grenzt

Abbildung 15: Verdecken von Namen

12345678910111213141516

main

using namespace std;

string begruessung (string name){ string hallo = "Hallo "; string begruessung = hallo + name; return begruessung;}

#include <iostream>#include <string>

int main (){ cout << begruessung ("Welt") << endl; return 0;}

/*** Beispiel sichtbarkeit.cpp ***/

begruessung name

hallobegruessung

#include <ostream>

1718

Page 45: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

über Vererbung verschiedene Gültigkeitsbereiche „aneinander gekoppelt“ werden. Mehr zu Klassen und Vererbung erfahren Sie in Abschnitt 4.1.6.

3.4 Typen und AusdrückeIn den letzten Abschnitten haben Sie Grundlegendes über den Aufbau von C++-Pro­grammen sowie über Namen und Deklarationen erfahren. Dabei sind Sie unweiger­lich über Typen gestolpert, etwa int oder string. Was ein Typ aber genau ist und welche es in C++ gibt, wissen Sie noch nicht (zumindest nicht aus diesem Skript). Dies holt dieser Abschnitt nach.

Wir wollen (Daten-)Typen zuerst anhand einer Analogie veranschaulichen. Ein Typ für eine Variable ist ungefähr wie eine Kuchenform für einen (bereits gebackenen) Kuchen. Die Kuchenform bestimmt, wie groß ein Kuchen maximal sein darf, der in die Form soll. Die Kuchenform verhindert dabei auch, dass Kuchen mit anderen Ausmaßen hineinpassen. So passt ein Sandkuchen, der in einer Kastenform gebacken wurde, normalerweise nicht in eine Biskuit-Form. Schließlich regelt die Kuchenform auch, wie mit dem darin enthaltenen Kuchen umzugehen ist. Beispielsweise schnei­den Sie obigen Sandkuchen in Scheiben, während Sie einen Biskuit vierteln oder achteln.

Übertragen auf Variablen regelt ein Datentyp,

(1) welche Werte die Variable aufnehmen kann (z. B. Zeichen oder Zahlen) [Gestalt der Kuchenform],

(2) welche Größe die Werte haben dürfen [Größe der Kuchenform],

(3) wie mit den Werten in dieser Variable umgegangen werden darf (z. B. welche Rechenoperationen erlaubt sind) [Eigenschaften der Kuchenform].

Die Analogie hinkt etwas, weil z. B. ein kleiner Sandkuchen in eine große Biskuit-Form durchaus hineinpassen könnte, wenn er nur klein genug ist. In C++ jedoch sind Form und Größe generell unabhängig voneinander.

Der oben zitierte „Umgang“ mit den Werten eines Datentyps erfolgt durch bestimmte Operationen. Bei den in C++ eingebauten Datentypen existieren diese Operationen nur in Form von bestimmten Operatoren. Ein Operator verändert einen Wert oder verknüpft zwei oder mehrere Werte zu einem neuen. Operatoren sind also im Prinzip Funktionen, die einen oder mehrere Argumente auf einen neuen Wert abbilden. Der Unterschied zu „normalen“ C++-Funktionen besteht lediglich in der Schreibweise: Während die Argumente von Funktionen immer durch Kommata getrennt innerhalb runder Klammern existieren und hinter dem Funktionsnamen stehen, können Argu­mente von Operatoren vor und hinter (und eventuell auch zwischen) diesen stehen.

Beispiel: Sie können sich den Ausdruck1 int ergebnis = 2 + (3 * 4);

hinter dem Gleichheitszeichen auch so vorstellen, dass die Operatoren + und * Funk­tionen mit zwei Argumenten sind, die ganze Zahlen erwarten und auch zurückliefern:

1 // Achtung: kein C++!2 int + (int argument1, int argument2);

37

ein Typ ist wie eine Kuchenform

Operatoren und Funktionen

Page 46: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

3 int * (int argument1, int argument2);4 int ergebnis = + (2, * (3, 4));

Wohlgemerkt, nur vorstellen: die obige Syntax ist in C++ nicht möglich.

Jeder Operator hat eine bestimmte Priorität. Ein Operator mit höherer Priorität wird eher ausgewertet als einer mit niedrigerer Priorität, wenn keine Klammern zur Rege­lung der Auswertungsreihenfolge vorhanden sind. Operatoren mit gleicher Priorität werden von links nach rechts ausgewertet.

Beispiel: Beim Auswerten des Ausdrucks 3 + 4 * 5 wird zuerst 4 * 5 ausge­wertet, weil * eine höhere Priorität als + hat. Erst danach wird der Operator + mit seinen Argumenten 3 und 20 ausgewertet. Will man die Auswertungsreihenfolge verändern, muss man geeignet Klammern setzen; im obigen Ausdruck also beispiels­weise (3 + 4) * 5, um zuerst 3 + 4 und danach 7 * 5 auszuwerten. In dem Ausdruck 3 + 4 - 5 hingegen haben alle Operatoren die gleiche Priorität. Es sind keine Klammern notwendig, und der Ausdruck wird von links nach rechts ausgewer­tet (d. h. erst 3 + 4 und dann 7 – 5).

Die arithmetischen Operatoren haben Prioritäten, die in der Mathematik üblich sind: Es gilt also Punkt- vor Strichrechnung, logisches UND (&&) vor logischem ODER (||) u. s. w. Eine Auflistung aller Operatoren nach ihrer Priorität finden Sie in Ta­belle 3.

Die Argumente von Operatoren (und allgemein von allen Operationen, inklusive ge­wöhnlicher Funktionen) sind Ausdrücke. Ein Ausdruck ist eine beliebig komplexe Aneinanderreihung von Zahlen, Variablen und Operatoren, die jedoch bestimmten Regeln gehorchen müssen. Insbesondere muss jeder Operator die jeweils passende Anzahl an Argumenten besitzen, und die Argumente müssen zueinander „passen“, d. h. Typ-gerecht sein.

Beispiel 1: Die Ausdrücke• 2• 5 + 6• alter – 18• gewicht*10000/(groesse*groesse)sind alle gültig (unter der Voraussetzung, dass groesse, gewicht und alter Variablen oder Konstanten eines Zahlen-Typs sind).

Beispiel 2: Die folgenden „Ausdrücke“ sind keine Ausdrücke oder scheitern an der geforderten Typ-Verträglichkeit:

• 2 + (der +-Operator benötigt zwei Operanden)

• 1 + (prozent / 100 (schließende Klammer fehlt)

• "Hallo" * 42 (Zeichenketten und Zahlen lassen sich nicht multiplizieren)

38

Priorität von Operatoren

Klammern setzen zum Ändern der Priorität

Ausdrücke

Page 47: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

39

Priorität Operatorhöchste Konstante (Zahl, Zeichen, Zeichenkette)

this(Ausdruck) (Regelung der Auswertungsreihenfolge)Bezeichner[ ] (Feldzugriff)( ) (Funktionsaufruf / Methodenaufruf / Konstruktoraufruf)., -> (Elementzugriff)++ (Postfix-Inkrement), -- (Postfix-Dekrement)dynamic_cast, static_cast,const_cast, reinterpret_cast (explizite Typumwandlung)typeid (Zugriff auf Typ-Informationen)

* (Zeiger-Derefenzierung), & (Adress-Operator)+ (Vorzeichen), - (Vorzeichen)! (logisches NICHT), ~ (binäres NICHT)++ (Präfix-Inkrement), -- (Präfix-Dekrement)sizeof, new (Speicher-Belegung), delete (Speicher-Freigabe)

(Typ) (explizite Typumwandlung)

.*, ->* (Elementzeiger-Zugriffsoperatoren)

*, /, % (Multiplikations- und Divisions-Operatoren)

+, - (Additions- und Subtraktions-Operatoren)

<<, >> (Schiebe-Operatoren)

<, >, <=, >= (Vergleich auf Größe)

== und != (Vergleich auf Gleichheit)

& (bitweises UND)

^ (bitweises XOR)

| (bitweises ODER)

&& (logisches UND)

|| (logisches ODER)

?: (Fallunterscheidung)

= (Zuweisung) und alle zusammengesetzten Formen (+= etc.)

throw (Ausnahme auswerfen)

niedrigste , (Komma)

Tabelle 3: Operatoren nach ihrer Priorität sortiert

Page 48: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

In Ausdrücken können noch weitere Operatoren und syntaktische Konstrukte vor­kommen, die sie in den nächsten Abschnitten und Kapiteln kennen lernen werden.

Ausdrücke sind schön und gut, aber wozu brauchen wir sie? Ausdrücke erlauben uns, Variablen, Konstanten, Funktionsaufrufe und andere Dinge miteinander in Bezie­hung zu setzen und zur Laufzeit des Programms auszuwerten. Auswerten bedeutet dabei, dass aus den vielen Teilen des Ausdrucks letztlich ein einziger Wert entsteht, der das Ergebnis des Ausdrucks darstellt.

Manche Ausdrücke können bereits vom Übersetzer ausgewertet werden, manche las­sen sich erst zur Laufzeit des Programms auswerten. Im obigen Beispiel gehören die ersten beiden Ausdrücke zur ersten Kategorie (2 wird zu 2, 5 + 6 zu 11 ausgewer­tet). Die beiden folgenden Ausdrücke gehören zur zweiten Kategorie, vorausgesetzt alter, groesse und gewicht sind Variablen, die erst zur Laufzeit gesetzt wer­den. Diese Ausdrücke können nicht während der Übersetzung ausgewertet werden, weil dann für diese Variablen noch keine Werte vorliegen. Zur Laufzeit hingegen werden diese Variablen mit Werten belegt, die z. B. vom Benutzer eingegeben wer­den. Gehen wir davon aus, dass in einem Test-Durchlauf die Werte für die Variablen alter, groesse und gewicht 18, 180 respektive 70 lauten, wird der erste der beiden Ausdrücke (alter - 18) zu 0, der zweite (gewicht*10000 / (gro­esse*groesse)) zu 21 gemäß der üblichen Rechenregeln (bei ganzzahliger Divi­sion wird abgerundet, s. Abschnitt 3.4.2.2) ausgewertet. Natürlich müssen diese „üb­lichen“ Rechenregeln für eine Programmiersprache wie C++ genau festgelegt werden. Die nächsten Abschnitte behandeln die verschiedenen Datentypen und die darauf erlaubten Operationen, so dass Sie danach verstehen können, was ein be­stimmter Ausdruck in einem C++-Programm bedeutet und wie er ausgewertet wird.

Wo kommen Ausdrücke nun in C++-Programmen vor? Fast überall, möchte man meinen. Insbesondere ist die Tatsache wichtig, dass jeder (gültige) Ausdruck eine Anweisung (3.5) ist und somit im Innern einer Funktion stehen kann. Das folgende Programm ist somit gültiges C++:

1 /*** Beispiel ausdruck.cpp ***/2 #include <string>3 using namespace std;45 int main ()6 {7 2; // dies ist ein Ausdruck8 3 + 4; // dies ebenfalls9 string ("Hallo") + string ("Welt"); // auch dies

10 return 0;11 }

Allerdings tut dieses Programm nicht wirklich etwas, weil die Ausdrücke in den Zei­len 7-9 keine Seiteneffekte haben. Ausdrücke als Anweisungen haben also nur dann Sinn, wenn sie Seiteneffekte produzieren, was eigentlich nur auf Funktionsaufrufe (3.6.2) und Zuweisungen (3.4.1) zutrifft. Alles andere ist Schall und Rauch.18

18) Es gibt sogar schlaue Übersetzer, die solche „sinnlosen“ Ausdrücke wegoptimieren, d. h. gar kei­nen Maschinen-Code dafür generieren. Diese Vorgehensweise ist gemäß dem C++-Standard auch erlaubt, vorausgesetzt der Übersetzer kann wirklich erkennen, dass ein Ausdruck keine Seitenef­

40

AusdrückeAusdrücke sind Anweisungen

Auswertung von Ausdrücken

Page 49: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

Allgemein gilt, dass wenn durch eine Operation der Wertebereichs des zugehörigen Typs verlassen wird, ein Fehler vorliegt. Dies kann auch bei einfachen Ausdrücken wie i + 1 geschehen, wenn i bereits den größtmöglichen Wert seines Datentyps inne hat. Seien Sie also bei Ihren Berechnungen vorsichtig, und überprüfen Sie immer, dass sie nicht aus Versehen den zulässigen Wertebereich verlassen.

Datentypen werden in C++ generell in zwei Klassen unterteilt: Fundamentale und zusammengesetzte Datentypen. Beide Klassen werden in den nächsten Abschnitten näher erläutert. Vorher jedoch betrachten wir einen ganz besonderen Operator: die Zuweisung.

3.4.1 Zuweisungen (offene und verkappte)Der Zuweisungsoperator ist so allgemein anwendbar, dass er es verdient, bereits an dieser Stelle erwähnt zu werden. Die Zuweisung ist der einzige eingebaute Operator, der Seiteneffekte produziert. Das bedeutet, dass Ausdrücke mit Zuweisungen nicht nur einen Wert wie „normale“ Funktionen zurückliefern, sondern zusätzlich die Da­ten des Programms verändern.

Zuweisungen haben generell den folgenden (vereinfachten) Aufbau:

Variable = Ausdruck

Wir verzichten hier auf ein Beispiel, weil es in den Beispiel-Programmen genügend Beispiele für eine Zuweisung gibt.

Zuweisungen sind nur dann erlaubt, wenn auf der linken Seite etwas steht, was ver­ändert werden darf. Somit scheiden beispielsweise Konstanten aus. Das folgende Beispiel produziert also einen Fehler bei der Übersetzung:

1 const double Pi = 3.14;2 Pi = 3.1415; // Fehler: Kann Konstante nicht verändern!

Sie sehen deutlich, dass Initialisierung (3.3.3) und Zuweisung zwei unterschiedliche Paar Schuhe sind. Dies wird noch deutlicher bei Objekten, denn nur bei der Initiali­sierung wird ein sogenannter Konstruktor (4.5.1) aufgerufen. Doch dazu später mehr.

Wichtig ist, dass der Ausdruck auf der rechten Seite des Zuweisungsoperators zum Datentyp der Variable auf der linken Seite passt. Ansonsten meldet der Übersetzer eine Typ-Verletzung und bricht die Übersetzung ab. Genaueres zur Typ-Verträglich­keit von Ausdrücken finden Sie in Abschnitt 3.4.5.

Zuweisungen sind selbst wieder Ausdrücke und können somit Teil anderer Ausdrücke sein. Eine Zuweisung wird zu dem Objekt ausgewertet, das bei der Zuweisung auf der linken Seite steht, und zwar nachdem das Objekt einen neuen Wert bekommen hat. Be­trachten Sie folgenden Ausdruck:

1 alter = groesse = 0;Hier werden beide Variablen auf Null gesetzt. Vollständig geklammert lautet der Aus­druck

1 (alter = (groesse = 0));und bedeutet soviel, dass zuerst groesse = 0 zu 0 (weil der Wert von groesse nach der Zuweisung 0 ist) und danach der Ausdruck alter = 0 (ebenfalls) zu 0 aus­

fekte erzeugt.

41

zwei Klassen von Datentypen

Zuweisungen

Zuweisungen und Typ-Verträglich­keit

Aufbau von Zu­weisungen

Verlassen des Wertebereichs

Zuweisung ist nicht gleich Initi­alisierung!

Zuweisungen be­nötigen ein ver­änderbares Ziel

Page 50: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

gewertet wird. Generell empfiehlt es sich aber nicht, Zuweisungen zu schachteln, wie es das obige Beispiel demonstriert.

Es gibt neben normalen Zuweisungen auch „Abkürzungen“ zwischen anderen Opera­toren und Zuweisungen, genannt zusammengesetzte Zuweisungen. Diese erlauben dem C++-Programmierer, Quelltext kompakter zu schreiben. Sie haben die Form:

Variable op= Ausdruck

wobei op einer der folgenden Operatoren ist: + - * / % & | ^ << >>. Ein solcher Ausdruck ist äquivalent zu:

Variable = Variable op Ausdruck

Beispiel: Wenn ergebnis eine Variable vom Typ int ist, haben die beiden Aus­drücke

1 ergebnis = ergebnis + 10

und1 ergebnis += 10

dieselbe Bedeutung.19

Schließlich gibt es zwei besondere Operatoren in C++, welche als verkappte Zuwei­sungen betrachtet werden können. Diese Operatoren dienen dem Inkrementieren (= Erhöhen) bzw. Dekrementieren (= Erniedrigen) um Eins und haben die Form:

++Variable

Variable++zum Inkrementieren bzw.

--Variable

Variable--zum Dekrementieren. Wenn diese Ausdrücke für sich allein stehen, entsprechen sie völlig dem Ausdruck

Variable += 1bzw.

Variable -= 1und unterscheiden sich nicht untereinander. Sie stellen also eine weitere Abkürzung dar, die besonders häufig bei for-Schleifen zur Erhöhung bzw. Erniedrigung der Schleifen-Variable gebraucht wird.

Die Ausdrücke ++Variable bzw. --Variable entsprechen jeweils vollständig dem Ausdruck

Variable += 1bzw.

19) Dies trifft nicht unbedingt bei Überladung (7.1) und Ausdrücken mit Seiteneffekten (3.4.1) zu!

42

zusammengesetz­te Zuweisungen

Inkrement- und Dekrement-Ope­ratoren

Prä-Inkrement und Post-Inkre­ment-Operatoren (bzw. Dekrement-Operatoren)

Page 51: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

Variable -= 1Die Ausdrücke Variable++ und Variable-- haben jedoch eine etwas andere Be­deutung: Während die Variante mit dem voran stehenden Operator zu dem Wert nach der Zuweisung ausgewertet wird, wird der Ausdruck mit dem nachstehenden Operator zu dem Wert vor der Zuweisung ausgewertet. Deswegen wird der erste Operator-Typ Prä-Inkrement- (bzw. Prä-Dekrement-)Operator und der zweite Post-Inkrement- (bzw. Post-Inkrement-)Operator genannt. Ein Beispiel veranschaulicht dies am besten:

1 /*** Beispiel incdec.cpp ***/2 #include <ostream>3 #include <iostream>4 using namespace std;56 int main ()7 {8 int alterVonPeter = 12;9 int alterVonMarianne = 8;10 int neuesAlterVonPeter = ++alterVonPeter;11 int neuesAlterVonMarianne = alterVonMarianne++;12 cout13 << alterVonPeter << " "14 << alterVonMarianne << " "15 << neuesAlterVonPeter << " "16 << neuesAlterVonMarianne << " "17 << endl;18 return 0;19 }

Nach dem Ausführen der vier Anweisungen sind alterVonPeter und alterVon­Marianne erwartungsgemäß um Eins erhöht, also 13 bzw. 9. Jedoch ist nur neu­esAlterVonPeter ebenfalls 13; neuesAlterVonMarianne hat den alten Wert von alterVonMarianne zugewiesen bekommen. Die Ausgabe des Programms ist demzufolge:

13 9 13 8Die Bedeutung der Dekrement-Operatoren ist analog.

3.4.2 Fundamentale DatentypenFundamentale Datentypen sind diejenigen Datentypen, die

(1) in der Sprache C++ fest eingebaut sind und

(2) nicht mehr in einfachere Typen zerlegt werden können.

Es lassen sich fünf Arten von fundamentalen Typen bilden, die in den nächsten Ab­schnitten genauer untersucht werden. Zu jedem Datentyp bzw. jeder Klasse von Da­tentypen werden die erlaubten Wertebereiche und Operationen vorgestellt und erläu­tert.

3.4.2.1 Datentypen für Zeichen und Zeichenketten

3.4.2.1.1 Wertebereich

Hierunter fallen alle in C++ eingebauten Datentypen, die sich zur Speicherung und Verarbeitung von Zeichen eignen. Der wichtigste dieser Datentypen ist der Typ char. Welche Zeichen Variablen dieses Typs nun speichern können, ist nicht genau definiert; C++ garantiert allerdings, dass die folgenden Zeichen gespeichert werden

43

der Datentyp char für ge­wöhnliche Zei­chen

Page 52: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

können (egal, welchen Zeichensatz die zugrunde liegende Rechner-Architektur ver­wendet):

• alle 26 lateinischen Buchstaben in Groß- und Kleinschreibung

• alle zehn Ziffern

• die folgenden Sonderzeichen:_ + - * / % ! ~ & | ^ < > = ? , ; . :" ' # ( ) [ ] { } \

• das Leerzeichen

• die folgenden Steuerzeichen:

Horizontaler Tabulator (\t), Zeilenumbruch (\n), Vertikaler Tabulator (\v), Formularvorschub (\f), Wagenrücklauf (\r), Alarm (\a), Rückschritt (\b)

Es ist nicht schlimm, wenn Sie mit einigen Steuerzeichen nichts anfangen können. Viele dieser Steuerzeichen sind lediglich esoterisch und haben nur in speziellen An­wendungen überhaupt eine Daseinsberechtigung. Sie sind hauptsächlich aus histori­schen Gründen im Sprachkern von C++ verankert. Die wichtigeren Steuerzeichen sind (horizontaler) Tabulator und Zeilenumbruch (den Sie bereits kennen gelernt ha­ben).

Wie Sie sehen, garantiert C++ nicht, dass Umlaute und andere Sonderzeichen in char-Variablen gespeichert werden können. Wenn Sie für einen „gewöhnlichen“ PC pro­grammieren, dann kann der Datentyp char für gewöhnlich alle 128 Zeichen des AS­CII-Zeichensatzes aufnehmen und viele Zeichen aus dem ISO 8859-1-Zeichensatz, der auch die wichtigsten Umlaute enthält. Dies wird jedoch nicht von der Sprache garan­tiert, sondern ist von der verwendeten C++-Implementierung abhängig. Sie müssen also damit rechnen, dass ein Programm, das Umlaute und andere oben nicht aufgeführte Zei­chen verwendet, auf anderen Rechner-Architekturen nicht übersetzt werden kann oder „Zeichensalat“ auftritt. Die beste Lösung zur Umgehung dieses Problems ist es, mit dem Datentyp wchar_t zu arbeiten (s. u.), der einen wesentlich größeren Zeichenbereich umfasst.

Ein weiterer wichtiger Zeichen Datentyp ist wchar_t. Dieser unterscheidet sich von char dadurch, dass er einen wesentlich größeren Wertebereich hat. Insbesonde­re kann er jedes Zeichen aus dem Unicode20-Zeichensatz aufnehmen. Sie sollten aber bedenken, dass die Wahl des größeren Zeichensatzes auf Ihr ganzes Programm Ein­fluss nimmt bzw. nehmen sollte. Nichts ist ärgerlicher, als dass der Anwender an ei­ner Stelle Umlaute in Dialogen, Dateien u. ä. verwenden kann und an einer anderen Stelle nicht. Entscheiden Sie sich also möglichst frühzeitig, ob Sie Unicode für Ihre Anwendung benutzen wollen oder nicht. Falls Sie die Unicode-Varianten der Daten­typen benutzen wollen, sollten Sie sich auch mit den Unicode-Varianten der Ein- und Ausgabe-Ströme vertraut machen (Tabelle 4). Für die Anwendungen in diesem Skript werden wir nicht weiter auf Unicode, wchar_t und wstring (s. u.) einge­hen.

20) s. http://www.unicode.org/

44

wichtige und we­niger wichtige Steuerzeichen

die Umlaut-Pro­blematik

wchar_t für Unicode

Page 53: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

Java-Programmierer müssen etwas umdenken: Der Java-Datentyp char entspricht dem C++-Datentyp wchar_t. Einen Java-Pendant zum C++-Datentyp char gibt es hinge­gen nicht.

Es gibt in C++ keinen fundamentalen Datentyp für Zeichenketten. Dies unterscheidet C++ von vielen anderen Programmiersprachen. Es gibt jedoch zwei Klassen in der C++-Standard-Bibliothek, die Zeichenketten repräsentieren und Operationen zur Ma­nipulation von Zeichenketten anbieten: die Klasse string (zur Speicherung von char-Zeichenketten) und die Klasse wstring (zur Speicherung von wchar_t-Zeichenketten); diese werden in Abschnitt 8.3 beschrieben.

Es ist jedoch wichtig, dass Zeichenketten-Konstanten wie "Hallo" oder L"Hallo" nicht automatisch den Typ string bzw. wstring besitzen. Sie sollten also stets string- bzw. wstring-Objekte definieren, wenn Sie mit Zeichenketten arbeiten wollen, etwa wenn Sie sie mit Hilfe des +-Operators aneinander hängen wollen (auch konkatenieren genannt):

1 using namespace std;2 // falsch: Sie können Felder nicht addieren, s. nächsten Einschnitt3 string ausgabe = "Hallo " + "Welt!";4 // in Ordnung, string-Objekte verstehen den +-Operator5 string abba = string("AB")+string("BA");6 // ebenfalls in Ordnung7 string hallo = "Hallo ";8 string welt = "Welt!";9 string ausgabe = hallo + welt;

Zeichenketten-Konstanten besitzen in C++ den Datentyp const char [n]. Dieser Datentyp beschreibt ein Feld von n nicht veränderbaren Zeichen, wobei n die Anzahl der in der Zeichenkette existierenden Zeichen + 1 darstellt. Das zusätzliche Zeichen wird als Abschluss-Zeichen benötigt, damit C++ das Ende der Zeichenkette finden kann, da C++-Felder nicht über die Anzahl der gespeicherten Elemente Buch führen. C++-Felder werden kurz in Abschnitt 3.4.3.5 behandelt.

3.4.2.1.2 Operationen

Die einzigen Operationen, die sich auf Zeichen sinnvoll anwenden lassen, sind Ver­gleiche zweier Zeichen (Tabelle 5) sowie Addition und Subtraktion von ganzen Zah­len. Bei dem Größer/Kleiner-Vergleich von Zeichen ist zu beachten, dass die zugrun­de liegende Ordnung vom jeweiligen Zeichensatz abhängt. Die Sprache garantiert lediglich,

• dass alle Großbuchstaben gemäß der üblichen lexikographischen Ordnung sor­tiert sind:

'A' < 'B' < 'C' < ... < 'Y' < 'Z'

45

die Sache mit den Zeichenketten

Vergleich von Zeichen

char-basierter Ein-/Aus­gabestrom

wchar_t-Pendant

std::cin std::wcinstd::cout std::wcoutstd::cerr std::wcerr

Tabelle 4: char-basierte Ein-/Ausgabeströme und ihre wchar_t-Pendanten

Page 54: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

• dass alle Kleinbuchstaben ebenso entsprechend sortiert sind,

• dass alle Ziffern ihrem Wert nach aufsteigend sortiert sind und unmittelbar auf­einander folgen:

'0' < '1' < '2' < ... < '8' < '9'

Alles andere ist vom Zeichensatz abhängig. Insbesondere ist nicht garantiert, dass die Buchstaben unmittelbar aufeinander folgen.21

Merksatz 6: Mach dich möglichst nicht von lokalen Eigenheiten abhängig!

So gilt beispielsweise im weit verbreiteten ASCII-Zeichensatz, der auch von VC++ be­nutzt wird, dass die Großbuchstaben vor den Kleinbuchstaben liegen, also dass 'Z' < 'a' gilt. Diese Tatsache ist durchaus nicht intuitiv. Andere Zeichensätze haben wieder­um andere Eigenarten.

Wie oben erwähnt, können Sie Zeichen und ganzzahlige Ausdrücke addieren bzw. subtrahieren. Dies ist besonders dann sinnvoll, wenn Sie über einen bestimmten Zei­chenbereich iterieren möchten, d. h. jedes Zeichen aus diesem Bereich in einer Schleife verarbeiten wollen. (Schleifen lernen Sie in Abschnitt 3.5.4 kennen.)

1 /*** Beispiel ziffern.cpp ***/2 #include <ostream>3 #include <iostream>

21) De facto ist dies z. B. im EBCDIC-Zeichensatz (der auf Großrechnern heute noch Verwendung findet) nicht der Fall; dort existieren „Lücken“ zwischen den Buchstaben ‘I’ und ‘J’ sowie zwi­schen ‘R’ und ‘S’.

46

Bewegen inner­halb von Zeichen­bereichen

Operator Bedeutung Beispiel== Vergleich auf

Gleichheit'a' == 'a' (liefert true)'x' == 'y' (liefert false)

!= Vergleich auf Un­gleichheit

'a' != 'a' (liefert false)'x' != 'y' (liefert true)

< Kleiner-als-Opera­tor

'a' < 'b' (liefert true)'b' < 'a' (liefert false)'a' < 'a' (liefert false)

> Größer-als-Opera­tor

'a' > 'b' (liefert false)'b' > 'a' (liefert true)'a' > 'a' (liefert false)

<= Kleiner-oder-gleich-Operator

'a' <= 'b' (liefert true)'b' <= 'a' (liefert false)'a' <= 'a' (liefert true)

>= Größer-oder-gleich-Operator

'a' >= 'b' (liefert false)'b' >= 'a' (liefert true)'a' >= 'a' (liefert true)

Tabelle 5: Relationale Operationen auf Zeichen

Page 55: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

4 #include <string>5 using namespace std;67 int main ()8 {9 // iteriere über alle Ziffern und verbinde sie10 string ziffern;11 for (char ziffer = '0'; ziffer <= '9'; ziffer = ziffer + 1)12 {13 // füge Ziffer ans Ende der Zeichenkette an14 ziffern = ziffern + ziffer;15 }16 cout << "Ziffern von 0 bis 9: " << ziffern << endl;17 return 0;18 }

In diesem Programm sind die entscheidenden Ausdrücke in Zeile 11 enthalten:

• char ziffer = '0' initialisiert die Schleifen-Variable ziffer mit dem Zeichen '0' als Startwert.

• ziffer = ziffer + 1 weist das Programm an, bei jedem Schleifen­durchlauf die Schleifen-Variable ziffer um ein Zeichen weiter zu „zählen“ (d. h. auf das nächste Zeichen im Zeichensatz zu setzen).

• ziffer <= '9' schließlich ist die Schleifen-Invariante. Wenn sie nicht mehr eingehalten werden kann (und das bedeutet in diesem Fall, dass die Variable ziffer über die '9' hinaus „gezählt“ hat), wird die Schleife verlassen.

Dass die Schleife alle Ziffern in der richtigen Reihenfolge und nur diese besucht, liegt an der obigen Garantie, dass die Ziffern ihrem Wert nach sortiert sind und un­mittelbar ohne „Lücken“ aufeinander folgen.

Objekte der Typen string und wstring unterstützen eine ganze Menge von Ope­rationen. Die wichtigsten an dieser Stelle sind Element-Zugriff, Konkatenation (Ver­kettung), Vergleiche und Längen-Bestimmung. Die nachfolgenden Beispiele bezie­hen sich auf diese Zeichenketten-Definitionen:

1 string hallo = "Hallo ";2 string halloKlein = "hallo ";3 string welt = "Welt!";

• Über den Operator [] greifen Sie auf einzelne Elemente eines string- oder wstring-Objekts zu, wobei die Position der einzelnen Zeichen mit Null an­fängt.

Beispiel: hallo [1] liefert 'a', das zweite (!) Zeichen der Zeichenkette.

• Den Operator + verwenden Sie, um Zeichenketten-Objekte aneinander zu reihen.

Beispiel: hallo + welt ergibt die Zeichenkette "Hallo Welt!".

Dies funktioniert auch für die Konkatenation von Zeichenketten und einzelnen Zeichen.

Beispiel: hallo + 'X' ergibt die Zeichenkette "HalloX".

47

Operationen auf Zeichenketten

Page 56: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

• Die Operatoren ==, !=, <, >, <= und >= führen Vergleiche von Zeichenketten durch. Dabei werden die Zeichenketten lexikographisch verglichen, d. h. Zeichen für Zeichen, bis ein Unterschied entdeckt wird oder das Ende einer Zeichenkette erreicht ist.

Beispiele: hallo == welt ergibt false, hallo != welt ergibt true, hallo < welt ergibt true (weil der erste Buchstabe (H) von hallo im Zei­chensatz vor dem ersten Buchstaben von welt (W) liegt), hallo < (hallo + welt) ergibt true (da die hallo-Zeichenkette kürzer ist), hallo == halloKlein ergibt false (weil die Vergleiche auf Groß- und Kleinschrei­bung achten).

• Die Operation length gibt die Länge der betrachteten Zeichenkette zurück. (Operationen und Methoden von Klassen werden in den Abschnitten 4.6.1 resp. 4.4.5.3 eingeführt.)

Beispiele: hallo.length() ergibt 6 (das Leerzeichen wird natürlich mitge­zählt!), welt.length() ergibt 5 und (hallo+welt).length() ergibt 11.

Bitte beachten Sie, dass die genannten Operationen (bis auf den Element-Zugriff) nur für Objekte des Typs string bzw. wstring existieren und nicht für „reine“ Zeichen­ketten-Konstanten. Sie können also nicht "Hallo " + "Welt!" schreiben – Ihr Übersetzer wird etwas wie „kann Zeiger nicht addieren“ von sich geben und die Über­setzung abbrechen.

3.4.2.2 Datentypen für Ganzzahlen

3.4.2.2.1 Wertebereich

In C++ gibt es vielfältige Datentypen, um ganze Zahlen darzustellen und mit ihnen zu rechnen. Die Datentypen unterschieden sich im Prinzip nur im unterstützten Wer­tebereich. Tabelle 6 gibt einen Überblick über die verschiedenen Typen. Die Werte­

bereiche sind geschlossene Intervalle, d. h. die angegebenen Grenzen sind in den je­weiligen Wertebereichen mit enthalten.

48

Datentypen für ganze Zahlen

Name Garantierter Wertebereichint wie short oder longunsigned int wie unsigned short oder unsigned longshort -32767 bis +32767unsigned short 0 bis 65535long -2147483647 bis +2147483647unsigned long 0 bis +4294967295

Tabelle 6: C++-Datentypen für Ganzzahlen

Page 57: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

Vielleicht verwundert Sie, dass der Datentyp int so ungenau spezifiziert ist. Dazu muss man verstehen, dass der Datentyp int derjenige sein soll, mit dem auf der ver­wendeten Rechner-Architektur am effizientesten gerechnet werden kann. Deshalb entspricht er entweder short oder long, je nach der zugrunde liegenden Architek­tur.

Diese Entsprechung der Datentypen wird jedoch rein intern vom Übersetzer durchge­führt und hat keinerlei Auswirkungen auf die C++-Sprachkonzepte. In C++ sind die Da­tentypen int und long immer verschieden, auch wenn sie der Übersetzer letztlich auf dieselbe Repräsentation (d. h. dieselbe Bitanzahl und dieselben Prozessor-Befehle) ab­bildet.

Der VC++-Übersetzer bildet die int- und long-Datentypem intern auf dieselbe Re­präsentation ab. Der int-Datentyp hat somit einen Wertebereich, der mindestens den Bereich von -2147483647 bis +2147483647 umfasst.

Wie Sie sehen, hat jeder vorzeichenbehaftete Datentyp eine vorzeichenlose un­signed-Variante. Diese kann genauso viele Werte aufnehmen wie der korrespon­dierende vorzeichenbehaftete Datentyp, allerdings fängt der Wertebereich generell bei Null an. Damit sind nur positive Werte erlaubt.

Schließlich sollten Sie wissen, dass die oben angegebenen Wertebereiche Mindestga­rantien sind, in verschiedenen C++-Implementierungen aber durchaus größer sein können. Auf Großrechnern ist es durchaus üblich, dem Datentyp long 64 Bit zu spendieren, was auf einen Wertebereich von -9223372036854775808 bis ein­schließlich +9223372036854775807 hinausläuft.

Welchen Datentyp sollten Sie nun am besten verwenden? Für gewöhnlich sollten Sie int für das Arbeiten mit ganzen Zahlen benutzen, da er für die verwendete Rechner-Architektur der „natürlichste“ Datentyp ist. Die vorzeichenlosen Varianten sollten Sie nur in Spezialfällen benutzen, etwa für Bitfelder (die in dem Skript auch gar nicht weiter behandelt werden). Haben Sie jedoch ganz bestimmte Anforderungen an Grö­ße und Genauigkeit des Datentyps (etwa weil Sie ein Programm für kaufmännische, wissenschaftliche o. ä. Berechnungen entwickeln), sollten Sie eine speziell für diesen Zweck entwickelte Bibliothek nutzen, welche die entsprechenden Datentypen „mit­bringt“.

Zahlen-Konstanten haben in C++ erst einmal den Typ int, es sei denn, die Konstan­te ist größer als der für int zulässige Wertebereich. Details hierzu finden Sie in Ab­schnitt 3.2.5. Allgemein sollten Sie vermeiden, Konstanten zu verwenden, die außer­halb des Wertebereichs für den Datentyp int liegen.

Der Datentyp char kann in C++ ebenfalls ganze Zahlen aufnehmen, allerdings ist der garantierte Wertebereich viel kleiner – er reicht von -128 bis 127 bzw. von 0 bis 256. Es ist jedoch leider nicht spezifiziert, ob char vorzeichenbehaftet oder vorzeichenlos ist, weshalb er für die meisten Rechenoperationen uninteressant ist.

3.4.2.2.2 Operationen

Folgende Operationen sind auf Ganzzahlen definiert:

49

int, ein ganz be­sonderer Daten­typ

vorzeichenbehaf­tete und vorzei­chenlose Typen

mehr über Werte­bereiche

Welcher Datentyp für welchen Zweck?

Zahlen-Konstan­ten

Page 58: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

(1) Arithmetische Operationen: Die Operatoren + (Addition), - (Subtraktion), * (Multiplikation), / (Division) und % (Rest bei Division) sind für alle ganzzahli­gen Datentypen definiert. Die Ergebnisse entsprechen den üblichen Rechenre­geln, wobei zu beachten ist, dass bei der Division von zwei positiven ganzen Zahlen generell in Richtung Null gerundet wird. Ist mindestens einer der Argu­mente negativ, ist jedoch nicht definiert, wie gerundet wird (Tabelle 7). Dement­sprechend kann der Rest bei der Division ein unterschiedliches Vorzeichen be­kommen (Tabelle 8). Es wird jedoch garantiert, dass unabhängig von der Wahl der Rundung stets der folgende Zusammenhang gilt:

(Dividend / Divisor) * Divisor + (Dividend % Divisor) = DividendVC++ rundet immer in Richtung Null. In Tabelle 7 und Tabelle 8 wird also bei den Er­gebnissen, bei denen eine Wahl besteht, immer das erste berechnet.

(2) Vorzeichen-Operatoren: Der Operator + „tut nichts“, er verändert sein Argu­ment nicht. Der Operator - negiert seinen Operanden. Beide Operatoren sind unär, d. h. sie haben nur ein Argument, und stehen vor ihrem Operanden.

(3) Relationale Operationen: Sie können die Operatoren aus Tabelle 9 verwenden, um Vergleiche von Zahlen durchzuführen. Alle diese Operationen liefern einen Wahrheitswert (true oder false) vom Typ bool zurück. Mehr dazu finden Sie in Abschnitt 3.4.2.4. Die zugrunde liegende Ordnung ist die (gewöhnliche) Ordnung auf den ganzen Zahlen.

50

Rechnen mit Zah­len

Vergleiche von Zahlen

Negieren

Ausdruck Ergebnis10 / 3 311 / 3 3 (nicht 4)-10 / 3 -3 oder -4-10 / -3 3 oder 410 / -3 -3 oder -4

Tabelle 7: Rechenregeln bei ganzzahliger Division

Ausdruck Ergebnis10 % 3 111 % 3 2-10 % 3 Wenn -10/3 = -3, dann -1, sonst 2-10 % -3 Wenn -10/-3 = 3, dann -1, sonst 210 % -3 Wenn 10/-3 = -3, dann 1, sonst -2

Tabelle 8: Rechenregeln bei ganzzahliger Division mit Rest

Page 59: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

Das Ergebnis eines solchen Vergleichs können Sie beispielsweise in einer Fall­unterscheidung nutzen (3.5.3.1):

1 /*** Beispiel if1.cpp ***/2 #include <istream>3 #include <ostream>4 #include <iostream>5 using namespace std;67 int main ()8 {9 cout << "Bitte geben Sie Ihr Alter ein: ";10 int alter;11 cin >> alter;12 if (alter < 0)13 cout << "Nicht sehr realistisch, oder?";14 else if (alter >= 0 && alter < 16)15 cout << "Hallo du da.";16 else if (alter >= 16 && alter < 18)17 cout << "Soll ich dich duzen oder Sie siezen?";18 else if (alter >= 18)19 cout << "Sie sind mir willkommen!";20 cout << endl;21 return 0;22 }

51

Operator Bedeutung Beispiel== Vergleich auf Gleichheit 5 == 7 (liefert false)

3 == 3 (liefert true)!= Vergleich auf Ungleichheit 5 != 7 (liefert true)

3 != 3 (liefert false)< Kleiner-als-Operator 5 < 7 (liefert true)

7 < 5 (liefert false)3 < 3 (liefert false)

> Größer-als-Operator 5 > 7 (liefert false)7 > 5 (liefert true)3 > 3 (liefert false)

<= Kleiner-oder-gleich-Ope­rator

5 <= 7 (liefert true)7 <= 5 (liefert false)3 <= 3 (liefert true)

>= Größer-oder-gleich-Opera­tor

5 >= 7 (liefert false)7 >= 5 (liefert true)3 >= 3 (liefert true)

Tabelle 9: Vergleiche von ganzen Zahlen

Page 60: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

(4) Bitweise Operationen: C++ unterstützt Operationen zum Manipulieren der Zah­len auf Bit-Ebene mit Hilfe der Operatoren ~ (bitweises NICHT), & (bitweises UND), | (bitweises ODER) und ^ (bitweises XOR). Weil diese Operationen aber sehr Hardware-abhängig sind und nur in speziellen Anwendungsbereichen sinnvoll sind, gehen wir nicht näher darauf ein.

(5) Schiebe-Operationen: Neben den bitweisen Operationen stellt C++ auch zwei Operatoren zur Verfügung, um die Bit-Repräsentation von Zahlen nach links (<<) bzw. nach rechts (>>) zu schieben. Da diese Operatoren ebenfalls nur in ei­nem begrenzten Umfeld Sinn machen, gehen wir auch auf diese Operationen nicht näher ein.

3.4.2.3 Datentypen für Fließkommazahlen

3.4.2.3.1 Wertebereich

C++ bietet drei verschiedene Datentypen für Fließkommazahlen an: float, dou­ble und long double (Tabelle 10, Tabelle 11).

Die Wertebereiche der Datentypen für Fließkommazahlen sind sehr Hardware-ab­hängig. Generell lässt sich sagen, dass der Datentyp double genauso viele oder mehr Werte als float und long double genauso viele oder mehr Werte als double umfasst. Ebenso ist long double mindestens so genau wie double und double mindestens so genau wie float. Es empfiehlt sich, für das Rechnen mit Fließkommazahlen den Datentyp double zu benutzen, wenn nicht gerade beson­ders hohe Anforderungen an Genauigkeit und Wertebereich existieren.

Der Datentyp float ist insofern problematisch, als dass er eine geringere garantierte Genauigkeit als der Datentyp long besitzt, so dass es passieren kann, dass Werte vom Typ long nicht verlustfrei in einer float-Variable gespeichert werden können. Da dies für die meisten Menschen sehr ungewöhnlich ist – schließlich hat der Datentyp

52

Operationen zur Bit-Manipulation

Name Garantierter Wertebereich(circa)

Typischer Wertebereich(circa)

float -1×1037 bis +1×1037 -1×1037 bis +1×1037

double -1×1037 bis +1×1037 -1×10308 bis +1×10308

long double -1×1037 bis +1×1037 -1×104092 bis +1×104092

Tabelle 10: Garantierte und typische Wertebereiche von Fließkommazahlen

Name Garantierte Genauigkeit Typische Genauigkeitfloat 6 Stellen 6 Stellendouble 10 Stellen 15 Stellenlong double 10 Stellen 19 Stellen

Tabelle 11: Garantierte und typische Genauigkeiten von Fließkommazahlen

Datentypen für Gleitkomma-Zah­len

Page 61: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

float einen größeren Wertebereich als long – sollte man den Datentyp float mög­lichst vermeiden.

3.4.2.3.2 Operationen

Die möglichen Operatoren für Fließkommazahlen entsprechen denen auf Ganzzah­len, mit den folgenden Ausnahmen:

• Es gibt keine Division mit Rest (%) bei Fließkommazahlen.

• Es gibt keine bitweisen und Schiebe-Operatoren für Fließkommazahlen.

• Das Runden bei allen arithmetischen Operationen erfolgt auf den am genauesten darstellbaren Wert.

Vorsicht ist beim Vergleich von Fließkommazahlen geboten. Auf Grund der internen Darstellung von Fließkommazahlen können sehr schnell Ungenauigkeiten entstehen. Es ist beispielsweise nicht garantiert, dass das zehnfache Erhöhen einer Fließkomma-Va­riable dasselbe ergibt wie das einmalige Erhöhen mit dem zehnfachen Wert (s. u.) Durch Rundungsfehler können unterschiedliche Ergebnisse entstehen.

1 /*** Beispiel fliesskomma.cpp ***/2 #include <ostream>3 #include <iostream>45 using namespace std;6 int main ()7 {8 float a = 0.0;9 float b = 0.0;10 float increment = 0.0001f; // f-Suffix forciert float-Typ11 int loopCount = 10000;1213 // a wird direkt um (increment * loopCount) erhöht14 a += increment * loopCount;15 // b wird „loopCount“-mal um „increment“ erhöht16 for (int i = 0; i < loopCount; ++i)17 b += increment;18 // überprüfe, ob a und b denselben Wert haben19 if (a == b)20 cout << "a und b sind gleich : ";21 else22 cout << "a und b sind ungleich! : ";23 cout << "a = " << a << ", b = " << b << endl;24 return 0;25 }

Das obige Programm kann durchaus bei einigen C++-Implementierungen und Rechner-Architekturen a und b sind ungleich ausgeben, obwohl vom mathematischen Standpunkt diese Antwort schlichtweg falsch ist.22 Deshalb sollte bei Fließkommazah­len statt auf genaue Gleichheit auf eine minimale Differenz geprüft werden, etwa so:

4 #include <cstdlib> // für die Funktion abs18 // überprüfe, ob a und b denselben Wert haben19 if (abs (a - b) < 0.0001)

VC++ gibt beim obigen Programm a und b sind ungleich : a = 1, b = 1.00005 aus; nach dem Abändern des Vergleichs wie angegeben wird die Ausgabe a und b sind gleich : a = 1, b = 1.00005 erzeugt.

22) Probieren Sie das Programm ruhig mit Ihrem bevorzugten Übersetzer aus!

53

Vergleiche von Fließkommazah­len sind proble­matisch

Page 62: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

3.4.2.4 Datentyp für Wahrheitswerte

3.4.2.4.1 Wertebereich

In C++ existiert ein Datentyp für Wahrheitswerte, genannt bool. Variablen und Konstanten von diesem Typ können nur zwei Werte annehmen, wahr und falsch. Diese werden durch die Konstanten true und false repräsentiert.

3.4.2.4.2 Operationen

Boolesche Werte lassen sich vergleichen, anordnen und mit logische Operatoren ver­knüpfen. Der Vergleich zweier Wahrheitswerte ist trivial; weiterhin gilt false < true; die Bedeutung der Operatoren >, <= und >= ist dann leicht abzuleiten.

Unterstützte logische Operationen sind logisches UND (& und &&), logisches ODER (| und ||) und logisches NICHT (!) mit den üblichen Bedeutungen. Der Unter­schied zwischen den beiden Versionen des UND- und ODER-Operators liegt darin begründet, dass die erste Version immer beide Argumente auswertet und dann das Ergebnis ermittelt, während die zweite Version eventuell schon vorher aufhört. Letz­teres tritt ein, wenn der erste Operand des &&-Operators bereits falsch ist; da das Er­gebnis unabhängig vom Wert des zweiten Operanden auf jeden Fall falsch ist, wird der zweite Operand gar nicht erst auswertet. Analoges gilt für den ||-Operator, wenn der erste Operand bereits wahr ist. Bei den &&- und ||-Operatoren spricht man wegen dieser „Abkürzungen“ auch von kurzgeschlossener Auswertung, bei & und | von vollständiger Auswertung.

Der letzte und vielleicht interessanteste Operator auf Wahrheitswerten ist der ?:-Operator. Er stellt eine funktionale Variante der Fallunterscheidung (if-Anweisung, s. Abschnitt 3.5.3.1) dar und hat die Form:

Bedingung ? Wenn-Ausdruck : Sonst-Ausdruck

Der Operator funktioniert folgendermaßen: Wenn Bedingung zum Zeitpunkt der Auswertung wahr ist, liefert der Operator den Wenn-Ausdruck zurück, ansonsten den Sonst-Ausdruck. Damit das funktionieren kann, müssen diese beiden Ausdrücke Typ-verträglich sein.

Beispiel: Nach dem Ausführen des folgenden Programm-Teils1 int alter = 18;2 string anrede = alter < 18 ? "du" : "Sie";

enthält die Variable anrede die Zeichenkette "Sie" aus dem Sonst-Teil, weil die Bedingung zu false ausgewertet wurde. Am Ergebnis ändert sich nichts, wenn die letzte Zeile statt dessen

2 string anrede = alter >= 18 ? "Sie" : "du";

heißt. Hier sind lediglich der Wenn- und Sonst-Teil vertauscht und die Bedingung lo­gisch verneint. Die Bedingung wird folglich zu true ausgewertet, und der Wenn-Teil des Operators wird ausgewählt.

54

bool, true und false

Wahrheitswerte lassen sich ver­gleichen und ord­nen

Boolesche Opera­toren; kurzge­schlossene und vollständige Aus­wertung

Operator zur Fallunterschei­dung

Page 63: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

Der ?:-Operator ist der einzige ternäre Operator (Operator mit drei Argumenten) in C++.

3.4.2.5 Der leere DatentypC++ kennt auch den leeren Datentyp namens void. Hier macht eine Aufteilung in Wertebereich und Operationen keinen Sinn, weil es weder Werte noch Operationen gibt. Wozu dann dieser Datentyp, werden Sie sich fragen? Es gibt diesen Datentyp hauptsächlich deshalb, weil C++ nicht zwischen Prozeduren und Funktionen unter­scheidet. Jede Funktion benötigt jedoch bei der Definition einen Rückgabetyp. Da Prozeduren aber nichts zurückgeben, muss dies geeignet angezeigt werden – eben mit Hilfe dieses Datentyps.

In vielen anderen Kontexten ist dieser Datentyp verboten; es gibt insbesondere keine Variablen oder Konstanten dieses Typs. Ausdrücke dieses Typs sind jedoch möglich; wenn eine Prozedur aufgerufen wird, ist das Ergebnis (logischerweise) vom Typ void. Dies kann ausgenutzt werden, um den Operator zur Fallunterscheidung (3.4.2.4) für Prozedur-Aufrufe zu nutzen, wie das folgende Programm zur Schau stellt:

1 /*** Beispiel void.cpp ***/2 #include <istream>3 #include <ostream>4 #include <iostream>5 #include <string>6 using namespace std;78 void begruesse (string anrede)9 {10 cout << "Hallo " << anrede << "!" << endl;11 }1213 int main ()14 {15 cout << "Alter: ";16 int alter;17 cin >> alter;18 // je nach Alter wird entweder „Sie“ oder „du“ als Argument an begruesse übergeben19 alter >= 18 ? begruesse ("Sie") : begruesse ("du");20 return 0;21 }

3.4.3 Zusammengesetzte DatentypenNeben den beschriebenen fundamentalen Datentypen existieren in C++ Konstrukte, um daraus abgeleitete Typen zu erstellen. Diese abgeleiteten Typen bestehen nicht nur aus einem (oder mehreren) Schlüsselwörtern, sondern erfordern den kombinier­ten Einsatz von Operatoren, Interpunktionszeichen und/oder Schlüsselwörtern. Diese deshalb zusammengesetzt genannten Datentypen werden in den nächsten Abschnit­ten näher erläutert.

55

der void-Daten­typ

Einschränkungen und Verwendung

abgeleitete Da­tentypen

Page 64: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

3.4.3.1 FunktionstypenFunktionen haben Sie bereits kennen gelernt. Der Operator, der in Deklarationen Funktionen bzw. Funktionstypen „erzeugt“, sind die beiden runden Klammern (()). Beispiel:

1 // dies ist eine Variable vom Typ int2 int anzahl;3 // dies ist eine Funktion ohne Parameter, die einen int-Wert zurückliefert4 int gibAnzahl ();

Allgemein haben Funktionstypen den Aufbau:

Rückgabetyp ( [Parameter-Liste] )Beispiel: Die obige Funktion gibAnzahl hat den Funktionstyp int().

Jede Funktion hat einen entsprechenden Funktionstyp. Die einzige sinnvolle Opera­tion auf Funktionen ist der Funktionsaufruf, den Sie bereits in einigen Beispiel-Pro­grammen kennen gelernt haben. Weitere Informationen dazu und zur Definition von Funktionen finden Sie in Abschnitt 3.6.

Funktionstypen werden Sie wahrscheinlich niemals direkt benutzen. Sie werden be­nötigt, wenn in einem Programm Verweise auf Funktionen gespeichert, verändert und weitergegeben werden, um beispielsweise Funktionen während der Laufzeit aus­zutauschen. Da die objektorientierten Fähigkeiten von C++ hier dem Programmierer wesentlich bessere Werkzeuge an die Hand legen, werden Funktionstypen und Funk­tionszeiger (3.4.3.3) bzw. Funktionsreferenzen (3.4.3.2) in C++ wenig benutzt.

Wenn Sie neugierig sind, wie Funktionstypen verwendet werden können, schauen Sie sich das folgende Programm an:

1 /*** Beispiel funktionstyp1.cpp ***/2 #include <ostream>3 #include <iostream>4 using namespace std;56 // addiert beide Operanden und gibt das Ergebnis zurück7 int add (int a, int b) {return a + b;}8 // subtrahiert beide Operanden voneinander und gibt das Ergebnis zurück9 int sub (int a, int b) {return a - b;}

10 // führt eine Berechnung mit beiden Operanden über die übergebene Funktion aus;11 // die Funktion muss den Typ int (int, int) besitzen12 int rechne (int a, int b, int op (int, int))13 {14 // rufe übergebene Funktion mit beiden Operanden auf15 return op (a, b);16 }1718 int main ()19 {20 // führe ein paar Berechnungen durch21 cout << "2 + 5 = " << rechne(2, 5, add) << endl;22 cout << "3 - 4 = " << rechne(3, 4, sub) << endl;23 return 0;24 }

Die interessanten Stellen sind einerseits die Definition der Funktion rechne in Zeile 12 und die Aufrufe der Funktionen in den Zeilen 15, 21 und 22. Die Funktion rechne

56

Funktionstypen

Aufbau von Funk­tionstypen

Operationen auf Funktionen

Funktionstypen sind eher unauf­fällig

Page 65: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

bekommt als dritten Parameter eine Funktion vom Typ int (int, int), die in Zei­le 15 wie eine normale Funktion aufgerufen wird. In den Zeilen 21 und 22 werden die Funktionen add und sub „ganz normal“ an die Funktion rechne als Argumente über­geben. Das ist möglich, weil die Typen der übergebenen Funktionen und der Typ des Parameters op übereinstimmen.

3.4.3.2 ReferenzenReferenzen sind versteckte Verweise auf andere (oder alternative Namen von ande­ren) Entitäten. Der zugehörige Typ hat die Form:

Typ &Es gibt keinerlei Operationen (außer der Initialisierung), die auf einer Referenz aus­geführt werden können: Alle Operationen werden immer mit der Entität durchge­führt, auf welche die Referenz verweist. Referenzen sind somit keine „Objekte“ im eigentlichen Sinn. Deshalb sind es „versteckte“ Verweise, weil Sie die Indirektion im Quelltext nicht bemerken (außer am & in der Deklaration).

Weil es keine leeren Verweise geben darf, müssen Referenzen immer initialisiert werden. Das Weglassen der Initialisierung ist ein Fehler.

Beispiel:1 /*** Beispiel ref.cpp ***/2 #include <ostream>3 #include <iostream>4 using namespace std;56 int main ()7 {8 // dies ist eine normale Ganzzahl-Variable9 int anzahl = 5;10 // dies ist eine Referenz auf diese Variable11 int &refAnzahl = anzahl;12 // gib die Werte beider Entitäten aus13 cout << anzahl << " " << refAnzahl << endl;14 // erhöhe anzahl15 ++anzahl;16 // gib die Werte beider Entitäten aus17 cout << anzahl << " " << refAnzahl << endl;18 // erhöhe refAnzahl19 ++refAnzahl;20 // gib die Werte beider Entitäten aus21 cout << anzahl << " " << refAnzahl << endl;22 return 0;23 }

Dieses Programm generiert die folgende Ausgabe:5 56 67 7

Wie Sie sehen, ist es völlig egal, ob Sie mit der Referenz oder der zugrunde liegen­den Variable arbeiten.23 Jetzt fragen Sie sich sicherlich, wofür Referenzen dann über­

23) Deshalb gibt es auch schlaue Übersetzer, die in einem solchen Fall die Referenz „wegoptimieren“ und alle Operationen direkt auf der Entität durchführen, auf welche die Referenz verweist.

57

Referenzen sind versteckte Ver­weise

Operationen auf Referenzen

Initialisierung

Page 66: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

haupt gut sind. Um es kurz zu machen: Sie haben ihre Daseinsberechtigung, aller­dings ist es an dieser Stelle schwierig, ein adäquates Beispiel zu zeigen. Referenzen werden hauptsächlich bei der Parameter-Übergabe bei Funktionen (3.6.4) und bei Elementen von Klassen (4.4.5.1) benutzt, deswegen werden sie dort in ihrem „natür­lichen Umfeld“ näher erläutert. Dann werden Sie auch den Sinn und Zweck von Re­ferenzen kennen lernen.

Java-Programmierer aufgepasst: Im Gegensatz zu C++ ist in Java das Referenz-Konzept implizit, d. h. in Java haben Sie keine Kontrolle darüber, wann über Referenzen und wann direkt auf Objekte bzw. Werte zugegriffen wird. Während in Java auf Objekte von Klassen immer über Referenzen und auf Elemente fundamentaler Datentypen immer di­rekt zugegriffen wird, haben Sie in C++ völlige Freiheit darüber. Insbesondere können Sie in C++ Referenzen auf Objekte fundamentaler Datentypen besitzen, was in Java nur über Objekte der entsprechenden Wrapper-Klassen (java.lang.Integer statt int, java.lang.Character statt char etc.) möglich ist.

Zusätzlich gibt es in Java sehr wohl Operationen, die sich auf die Referenzen „an sich“ und nicht auf die referenzierten Objekte bezieht: die Identitäts-Vergleiche und die Zu­weisung. Während der Java-Code

1 Integer i1 = new Integer (5);2 Integer i2 = new Integer (5);3 Integer i3 = i1;4 Integer i4 = i2;5 System.out.println (i3 == i4 ? "gleich" : "ungleich");

ungleich ausgibt, da die verglichenen Referenzen auf unterschiedliche Objekte ver­weisen, produziert der äquivalente (!?) C++-Code

1 int i1 = 5;2 int i2 = 5;3 int &i3 = i1;4 int &i4 = i2;5 cout << (i3 == i4 ? "gleich" : "ungleich") << endl;

die Ausgabe gleich, da in C++ die Inhalte der referenzierten Variablen verglichen werden und diese gleich sind. Ähnlich verhält es sich mit der Zuweisung: Während in Java die Zuweisung an eine objektwertige Variable diese an ein neues Objekt bindet, wird in C++ der Inhalt des Objekts kopiert.

Referenzen können in C++ nicht neu gebunden werden; das bedeutet, dass das Ziel einer Referenz nicht verändert werden kann. Wenn Sie dies benötigen, müssen Sie auf Zeiger (3.4.3.3) ausweichen.

3.4.3.3 ZeigerZeiger sind ebenfalls Verweise wie Referenzen, jedoch sind sie „echte“ Objekte in dem Sinne, dass sie ausgelesen und modifiziert werden können. Man könnte sie als veränderbare Referenzen bezeichnen. Sie haben die Form:

Typ *Zeiger unterstützen eine Vielzahl an Operationen. Neben der Zuweisung, um das Ziel eines Zeigers zu verändern, kann man Zeiger miteinander auf Gleichheit (==) und Ungleichheit (!=) vergleichen sowie mit dem !-Operator prüfen, ob ein Zeiger un­gleich dem Null-Zeiger (4.5.5.2) ist. Im Gegensatz zu Referenzen, die mit dem refe­renzierten Objekt initialisiert werden, muss bei der Initialisierung oder Zuweisung

58

Java-Referenzen: Unterschiede und Gemeinsamkeiten

Zeiger sind „ech­te“ Verweise

Operationen auf Zeigern; Adres­sen von Objekten

Page 67: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

von Zeigern die Adresse des Objekts übergeben werden. Die Adresse können Sie sich ruhig bildlich vorstellen: sie stellt den Ort dar, an dem die betreffende Entität zu finden ist. Die Adresse eines Objekts wird mit Hilfe des &-Operators bestimmt, der dem entsprechenden Objekt vorangestellt wird.

Der Operator & zum Bestimmen der Adresse eines Objekts hat nichts, aber auch wirk­lich gar nichts mit dem Typ-Modifizierer & zu tun, der zum Erzeugen von Referenz-Ty­pen verwendet wird. Die Ähnlichkeit ist dennoch nicht grundlos: Die Verwendung des & bei Referenz-Typen könnte als Hinweis darauf verstanden werden, dass Referenzen etwas ähnliches wie Zeiger sind und intern Adressen abspeichern.

Wenn auf den Inhalt des referenzierten Objekts zugegriffen werden soll, muss der Zeiger mit Hilfe des Operators * dereferenziert werden, der ebenfalls dem entspre­chenden Ausdruck vorangestellt wird. Der Operator * ist also die Umkehr-Operation zum Adress-Operator &. Daneben gibt es noch den Operator ->, der aber erst bei Objekten einer Klasse von Bedeutung wird (4.4.5.3).

Da das alles für Sie am Anfang wahrscheinlich ziemlich harter Tobak ist, schreiben wir das obige Referenz-Beispiel so um, dass statt Referenzen Zeiger eingesetzt wer­den:

1 /*** Beispiel ptr.cpp ***/2 #include <ostream>3 #include <iostream>4 using namespace std;56 int main ()7 {8 // dies ist eine normale Ganzzahl-Variable9 int anzahl = 5;10 // dies ist ein Zeiger auf diese Variable (beachten Sie den &-Operator)11 int *ptrAnzahl = &anzahl;12 // gib die Werte beider Entitäten aus (beachten Sie den *-Operator)13 cout << anzahl << " " << *ptrAnzahl << endl;14 // erhöhe anzahl15 ++anzahl;16 // gib die Werte beider Entitäten aus17 cout << anzahl << " " << *ptrAnzahl << endl;18 // erhöhe das Ziel von ptrAnzahl (beachten Sie die Reihenfolge der Operatoren!)19 ++*ptrAnzahl;20 // gib die Werte beider Entitäten aus21 cout << anzahl << " " << *ptrAnzahl << endl;22 return 0;23 }

Die Ausgabe ist dieselbe wie in dem Beispiel mit Referenzen.

Zeiger werden nicht nur benutzt, um Verweise auf lokale (3.3.4) Objekte zu spei­chern, sondern vor allem im Zusammenhang mit dynamischer Speicherverwaltung. Hierzu lesen Sie bitte den Abschnitt 4.5.5.

Der korrekte Umgang mit Zeigern ist nicht einfach. Es wird sogar als so schwierig an­gesehen, dass viele Programmiersprachen entweder kein Zeiger-Konzept besitzen (ur­sprüngliche BASIC-Dialekte wie GW-BASIC) oder es vor dem Programmierer „verste­cken“ (Java, Visual BASIC). Gleichwohl erfordern viele komplexere Datenstrukturen wie Listen oder Bäume Zeiger oder ähnliche Konzepte zur dynamischen Verwaltung von Objekten. Aus diesem Grund werden wir auf Zeiger näher in Abschnitt 4.5.5.4 ein­

59

Dereferenzierung von Zeigern

Page 68: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

gehen, in dem eine einfach verkettete Liste zur Verwaltung dynamisch allozierter Ob­jekte entwickelt wird.

Merksatz 7: Vermeide den unkontrollierten Umgang mit Zeigern!

3.4.3.4 KlassenKlassen (oder Stukturen bzw. Records) sind benutzerdefinierte Datentypen. Da Klas­sen in C++ die Grundlage objektorientierter Programmierung darstellen, müssen wir Sie auf Kapitel 4 vertrösten.

3.4.3.5 FelderC++ enthält in den Sprachkern eingebaute Felder. Beispiel:

1 /*** Beispiel feld.cpp ***/2 #include <ostream>3 #include <iostream>4 using namespace std;56 int main ()7 {8 // definiere die Anzahl der Elemente im Feld9 const int anzahl = 10;

10 // definiere ein Feld aus int-Werten mit „anzahl“ Elementen und initialisiere sie11 int zahlen [anzahl] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };12 // gib alle Elemente aus13 for (int element = 0; element < anzahl; ++element)14 {15 cout << "Element " << element << " = "16 << zahlen [element] << endl;17 }18 return 0;19 }

Wie Sie erkennen können, werden Felder wie normale Variablen deklariert, besitzen jedoch zusätzlich eine (konstante) Größenangabe in eckigen Klammern, welche die Anzahl der Elemente in dem Feld angibt (Zeile 11). Wenn Sie bereits bei der Defini­tion das Feld mit Werten belegen wollen, geht dies mit einer speziellen Syntax, wo­bei Sie innerhalb geschweifter Klammern die Initialwerte der Feldelemente angeben können (auch Zeile 11). Ebenfalls über eckige Klammern kann auf die Elemente zu­gegriffen werden, wobei ein entsprechender Index innerhalb der Klammern steht, der übrigens nicht konstant sein muss (Zeile 16). Felder können wie andere Variablen auch mit beliebigen Typen und Modifizierern wie const verknüpft werden, wobei sich beides auf die Elemente des Feldes bezieht. Ein const int-Feld ist also ein Feld aus const int-Elementen.

Diese Felder sind jedoch in ihrer Funktionsweise ziemlich beschränkt:

• Die Länge muss bei der Definition festgelegt werden und kann sich während des Programmablaufs nicht ändern.

• Die Länge eines Feldes muss bereits zur Übersetzungszeit feststehen und kann nicht zur Laufzeit ausgerechnet werden.

60

Klassen oder Strukturen

eingebaute Fel­der

Beschränkungen eingebauter Fel­der

Page 69: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

• Sie können keine Felder von einer Funktion zurückgeben lassen (nur Zeiger auf Felder).

• Beim Zugriff auf Elemente wird der Index nicht gegen die Länge des Feldes ge­prüft; wenn Sie sich beim Zugriff „verrechnet“ haben, kann dies zum Programm­absturz führen!

Die Beschränkungen liegen allesamt daran, dass Felder in C++ keine „echten“ Ob­jekte sind. Deshalb stellen wir in Abschnitt 8.3 eine Klasse der C++-Standard-Biblio­thek vor, welche die obigen Beschränkungen nicht hat und wesentlich sicherer und flexibler zu benutzen ist.

3.4.3.6 Andere zusammengesetzte DatentypenC++ kennt noch weitere benutzerdefinierte Datentypen, die aber in diesem Skript keine große Rolle spielen und deshalb hier nur kurz der Vollständigkeit halber er­wähnt werden:

• Aufzählungen: In C++ ist es möglich, eine Gruppe von symbolischen Konstan­ten (= Aufzählung) zu definieren und Typ-sicher zu benutzen. Beispiel:

1 /*** Beispiel enum.cpp ***/2 #include <istream>3 #include <ostream>4 #include <iostream>5 #include <string>6 using namespace std;78 // Definiere einen Aufzählungstyp zur Unterscheidung des Geschlechts9 enum Geschlecht10 {11 maennlich, // wenn nicht initialisiert, bekommt die erste Konstante den Wert 012 weiblich, // jede folgende Konstante bekommt den Wert der vorherigen + 113 unbekannt = 99 // man kann die Konstanten auch direkt initialisieren14 };15 // ab hier sind die Konstanten maennlich, weiblich und unbekannt verfügbar1617 // Gibt „Hallo Herr <name>“ oder „Hallo Frau <name>“ aus, je nach Geschlecht;18 // wenn „unbekannt“ als Geschlecht übergeben wird, gibt die Funktion „Hallo <name>“ aus19 void begruesse (string name, Geschlecht geschlecht)20 {21 if (geschlecht == maennlich)22 cout << "Hallo Herr " << name << endl;23 else if (geschlecht == weiblich)24 cout << "Hallo Frau " << name << endl;25 else26 cout << "Hallo " << name << endl;27 }2829 int main ()30 {31 // erfrage Name32 cout << "Name: ";33 string name;34 cin >> name;35 // erfrage Geschlecht36 cout << "Geschlecht (m/w): ";37 char geschlecht;

61

Aufzählungen sind symbolische Konstantengrup­pen

Page 70: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

38 cin >> geschlecht;39 // die switch- und break-Anweisungen lernen Sie im Abschnitt 3.5.3.2 kennen40 switch (geschlecht)41 {42 case 'm' :43 case 'M' :44 begruesse (name, maennlich);45 break;46 case 'w' :47 case 'W' :48 begruesse (name, weiblich);49 break;50 default :51 begruesse (name, unbekannt);52 break;53 }54 return 0;55 }

• Variante Records/Unions: C++ kennt, ähnlich Pascal, Strukturen, die während der Laufzeit zu verschiedenen Zeiten verschieden getypte Werte enthalten kön­nen (also z. B. mal eine Zeichenkette und mal eine Zahl). Diese Datenstruktur macht in C++ nur in sehr speziellen Anwendungsgebieten Sinn und wird deshalb nicht weiter erläutert.

3.4.4 Konstanten(Benannte) Konstanten sind Objekte wie Variablen, jedoch kann ihr Inhalt nach der Initialisierung nicht verändert werden. Konstanten werden wie Variablen definiert, nur wird ihrem Typ zusätzlich das Schlüsselwort const als Typ-Modifizierer vor­angestellt:

1 bool habeGeburtstag (); // wird weiter unten benötigt23 // Das ist eine Ganzzahl-Variable4 int alter = 18;5 if (habeGeburtstag ())6 alter++; // Variablen können verändert werden78 // Das ist eine Fließkommazahl-Konstante9 const double Pi = 3.141592654;

10 Pi = 4; // Fehler: Konstanten können nicht verändert werden

Konstanten sollten immer dann verwendet werden, wenn der Inhalt wirklich nicht verändert werden soll. Das kommt häufiger vor, als Sie vielleicht denken. Beispiels­weise sollte im ersten C++-Beispiel in Abschnitt 2.3 die Zeile 25

25 string meinName = liesName ();

so heißen:25 const string meinName = liesName ();

Dies ist besser, da meinName im restlichen Code nicht verändert wird (und verän­dert werden soll).

62

Unions

Konstanten

Konstanten vs. Variablen

Page 71: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

C++-Konstanten entsprechen in etwa final-Attributen in Java. Allerdings ist zu be­achten, dass final-Attribute innerhalb von Klassen anders initialisiert werden: In Java sind Zuweisungen im Konstruktor sowie Initialisierung in der Klassen-Definition er­laubt, in C++ ist eine Initialisierungsliste im Konstruktor nötig. Näheres hierzu steht im Abschnitt 4.5.1.

Das Verwenden von benannten Konstanten hat mehrere Vorteile:

(1) Lesbarkeit: Sie sehen im Quelltext sofort, welche Werte veränderlich sind und welche nicht. Dadurch bekommen Sie einen besseren Überblick über die Struk­tur des betrachteten Programmteils. Besonders wertvoll ist const bei Referenz-Parametern (3.6.4), weil dann bereits an der Funktions- oder Operations-Schnitt­stelle erkannt werden kann, ob die Funktion bzw. Operation die übergebenen Ar­gumente verändert oder nicht; siehe hierzu auch Abschnitt 4.4.7.1.

(2) Wartbarkeit: Auch Werte, die anfangs als konstant erachtet wurden, können sich mit der Zeit ändern (übliche Beispiele sind die Mehrwertsteuer o. ä.) Wurde der Wert in der Entwicklungsphase als benannte Konstante definiert und später über den Namen verwendet, so lässt sich eine Änderung sehr leicht durchführen, indem einfach der Initialisierungsausdruck der Konstante geändert wird. Haben Sie jedoch nicht benannte Zahlen-Konstanten quer über den Quelltext verteilt, müssen Sie jede einzelne Verwendung der entsprechenden Konstante suchen und ändern. Das ist nicht nur mühselig, sondern auch fehlerträchtig, weil Sie viel­leicht nicht alle Stellen finden.

(3) Fehlerprüfung: Der Übersetzer kann ihre Absichten besser „nachprüfen“. Nach dem Prinzip „vier Augen sehen mehr als zwei“ ist es möglich, dass Sie eine Va­riable nicht verändern möchten aber es dennoch (aus Versehen) tun. Haben Sie vorher das betreffende Objekt durch const zur Konstante gemacht, fängt der Übersetzer die inkorrekte Zuweisung ab und generiert eine Fehlermeldung. Ohne const kann es passieren, dass Sie diesen Fehler erst viel später merken (wahr­scheinlich, wenn es bereits zu spät ist und das Programm bereits beim Kunden ist.)

Merksatz 8: Verwende symbolische Konstanten!

Eine Alternative zum const-Modifizierer sind Aufzählungen (3.4.3.6), die eben­falls benannte Konstanten darstellen. Allerdings sind solcherart definierte Konstanten immer von einem Zahlen-Typ (int oder größer, je nach Wertebereich der Konstan­ten-Werte), so dass diese Möglichkeit für Konstanten anderer Typen (etwa Zeichen-, Zeichenketten-, oder Objekt-Konstanten) weg fällt.

3.4.5 Typ-Verträglichkeit und Konvertierungen

3.4.5.1 Typ-Verträglichkeit und implizite Typ-KonvertierungenAn vielen Stellen in C++ ist es erforderlich, dass ein Ausdruck den „passenden“ Typ hat. In einer Sprache wie C++, die über viele verschiedene fundamentale und zusam­mengesetzte Typen verfügt, wäre es ein großes Hindernis, wenn der „passende“ Typ

63

gute Gründe, Konstanten zu verwenden

Typ-Verträglich­keit darf nicht zu eng gefasst sein

Aufzählungen als Alternative

Page 72: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

lediglich exakt derselbe Typ sein müsste. In einem solchen Fall wäre sogar das fol­gende C++-Programm fehlerhaft:

1 // --- hypothetisches C++ mit strikten Typverträglichkeits-Regeln ---2 int main ()3 {4 short retCode = 0; // wäre falsch: int-Konstante → short-Variable5 return retCode; // wäre falsch: short-Ausdruck → int-Rückgabetyp6 }

Um solche Probleme zu vermeiden, definiert C++ verschiedene Regeln zur Typ-Ver­träglichkeit und Typ-Umwandlung. In diesem Abschnitt wollen wir nur die Verträg­lichkeit zwischen fundamentalen (und daraus zusammengesetzten) Typen untersu­chen; die Verträglichkeit zwischen verschiedenen Klassen-Typen wird in Abschnitt 4.6.3 behandelt.

Zuerst wollen wir erarbeiten, in welchem Kontext eine Typ-Verträglichkeit erforder­lich ist:

(1) Initialisierung von Konstanten oder Variablen (einschließlich Belegung von Pa­rametern mit Argumenten)

(2) Zuweisung von Ausdrücken zu Variablen

(3) arithmetische Operationen

Wir wollen die ersten beiden Fälle gemeinsam behandeln, da sie fast identisch sind. Wir bezeichnen den Ausdruck, mit dem initialisiert wird oder der zugewiesen wird, als Quelle und das Objekt, das initialisiert oder verändert wird, als Ziel. Dementspre­chend gibt es einen Quell- und einen Ziel-Typ. Nun versucht der Übersetzer, die

64

Wann wird eine Konvertierung benötigt?

Konvertierung bei Initialisierung und Zuweisung

Zieltyp →

Quelltyp ↓

Ganzzahl Fließkommazahl Zeichen Wahrheitswert

Ganzzahl eventuell nicht wert­erhaltend, s. u.

Konvertierung, even­tuell mit Genauig­keitsverlust

Konvertierung in das Zeichen an der ent­sprechenden Position im Zeichensatz

0 → false, alles andere → true

Fließkom­mazahl

Konvertierung durch Abschneiden der Nachkommastellen; Fehler wenn Ergebnis zu groß

Konvertierung durch Abschneiden der Nachkommastellen; Fehler wenn Ergebnis zu groß

wie Konvertierung erst in eine Ganzzahl und dann in ein Zei­chen; Fehler wenn Ergebnis zu groß

0 → false, alles andere → true

Zeichen Konvertierung in die Position des Zeichens im Zeichensatz

wie links Konvertierung von char nach wchar_t eventu­ell nicht werterhal­tend

wie Konvertierung erst in eine Ganzzahl und dann in einen Wahrheitswert

Wahr­heitswert

false → 0true → 1

false → 0true → 1

wie Konvertierung erst in eine Ganzzahl und dann in ein Zei­chen

keine Konvertierung notwendig

Tabelle 12: Konvertierungen zwischen fundamentalen Datentypen

Page 73: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

Quelle in einen Ausdruck des Ziel-Typs zu konvertieren. Diese Konvertierung ist tri­vial, wenn Quell- und Ziel-Typ identisch sind, ansonsten hat er mehr oder weniger etwas zu tun. Tabelle 12 fasst die möglichen Konvertierungen zusammen. Dabei gilt: je „röter“ das entsprechende Feld, umso gefährlicher ist die Konvertierung. Grün be­deutet „es kann nichts schiefgehen“, gelb bedeutet „hängt von den Werten ab, kann zu Verlusten, aber nicht zu Fehlern führen“ und rot bedeutet „wenn Sie Pech haben, kann zur Laufzeit des Programms ein Programmabsturz auftreten“.

Im Gegensatz zu anderen Programmiersprachen wie Java sind Konvertierungen zwi­schen Ganzzahl-Typen erlaubt, bei denen die Quelle nicht im Wertebereich des Zieltyps liegt. In einem solchen Fall wird die Quelle einfach geeignet „verkleinert“ (gemäß Mo­dulo-Arithmetik). Dies gilt aber nicht für Konvertierungen zwischen Fließkomma-Ty­pen oder zwischen Fließkomma- und Ganzzahl-Typ: wenn Sie dabei einen für den Ziel­typ zu großen Wert erhalten, kommt es zu einem Programmfehler.

Merksatz 9: Meide gefährliche Typ-Umwandlungen!

Allgemein gilt, dass zwischen den Datentypen eine (partielle) Ordnung besteht, die in Abbildung 16 dargestellt wird. Zum Verständnis: Wenn eine Verbindung (direkt oder indirekt) von einem Typ zum anderen existiert, bedeutet dies, dass Ausdrücke des Quelltyps uneingeschränkt ohne Verluste in den Zieltyp konvertiert werden kön­nen. Diese Konvertierungen werden als sicher bezeichnet, weil sie nie zu Wertver­lust, Verlust der Genauigkeit oder Programmabbrüchen führen können.

Wenn zusätzliche Annahmen gemacht werden können, etwa ob der Datentyp int dem Datentyp short oder dem Datentyp long entspricht, sind eventuell weitere Konvertierungen möglich.

In VC++ gilt beispielsweise, dass die Datentypen int und long dieselbe interne Re­präsentation und somit denselben Wertebereich besitzen. Ebenso verhält es sich mit den vorzeichenlosen Varianten unsigned und unsigned long, mit double und long double sowie mit wchar_t und unsigned short. Des Weiteren ist der Typ char vorzeichenbehaftet und entspricht somit signed char. Schließlich ist der Wer­tebereich des Typs char echt kleiner als der von short, und der letztere echt kleiner

65

Verlustbehaftete Ganzzahl-Kon­vertierungen

Anordnung der fundamentalen Datentypen

Anordnung der fundamentalen Datentypen in VC++

Abbildung 16: Verlustfreie Konvertierungen in C++

unsignedlong

unsignedint

unsignedshort

unsignedchar

longintshortsignedchar

float longdoubledouble

char wchar_tbool

Page 74: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

als der von long. Daraus lassen sich die folgenden sicheren Konvertierungen für VC++ ableiten (Abbildung 17). Beachten Sie aber, dass Ihre Programme nicht von diesem Wissen abhängen sollten, wenn Sie vorhaben, ihre Programme eines Tages unter einem anderen Übersetzer oder auf einer anderen Hardware-Architektur ablaufen zu lassen!

Arithmetische Operationen sind im Kontext von Konvertierungen etwas besonderes, weil sie zwei Operanden in ein Resultat überführen. Wir haben es also mit zwei Quellen (und Quell-Typen) zu tun. Ohne tiefer in die Details zu gehen, halten wir an dieser Stelle fest, dass beide Operanden in den Typ konvertiert werden, der beide Quell-Typen umfasst, ohne dass es zu Wertverlusten kommt. Die genauen Regeln sind ziemlich komplex und langweilig; wenn Sie neugierig sind, schauen Sie in Ih­rem Lieblings-C++-Buch nach...

Die Verträglichkeit von Zeigern gehorcht ebenfalls bestimmten Regeln. Es ist am einfachsten, wenn man sagt, dass die Typen zweier Zeiger, die einander zugewiesen werden sollen, identisch sein müssen. Danach kann man die Ausnahmen studieren:

• Der Ziel-Typ kann mehr „const“ sein. Beispiel:int i = 1;const int *pi = &i; // OK, int* --> const int* ist gültige Konvertierung

• Der Ziel-Typ kann allgemeiner sein. Dies ist erst im Kontext von Klassen und Spezialisierung verständlich (4.6.3); es ist nur der Vollständigkeit halber bereits an dieser Stelle erwähnt.

Es gibt noch weitere Einschränkungen, die aber so speziell sind, dass wir sie hier nicht weiter behandeln.

3.4.5.2 Explizite Typ-Konvertierungen (Casting)Es gibt in C++ die Möglichkeit, die Konvertierung eines Ausdrucks von einem Typ in einen anderen explizit anzugeben. Dies wird im Allgemeinen Casting bzw. Casten genannt; ein Cast ist die Bezeichnung für eine explizite Typ-Umwandlung. Die Syn­

66

Konvertierung bei arithmeti­schen Operatio­nen

Verträglichkeit von Zeigern

explizite Typ-Um­wandlungen

Abbildung 17: Verlustfreie Konvertierungen in VC++

unsignedlong

unsignedint

unsignedshort

unsignedchar

longintshortsignedchar

float longdoubledouble

char wchar_tbool

Page 75: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

tax für eine solche explizite Typ-Umwandlung ist einfach: Man schreibt den ge­wünschten Typ in runden Klammern vor den Ausdruck. Beispiel:

1 int i = 1;2 char c = (char) i; // int-Wert in Typ char explizit umgewandelt

In Zeile 2 wird der Wert der int-Variable i in den Datentyp char umgewandelt.

Die gezeigte Syntax ist die einfachste, leider auch die für den Übersetzer und den Menschen am schwersten „auffindbare“: Da die Sprache C++ vor Klammern nur so „strotzt“, sind explizite Umwandlungen in der oben gezeigten Klammern-Syntax nur schwer im Quelltext auszumachen. Deshalb wurden C++ vier weitere Cast-Operato­ren spendiert:

• static_cast < Ziel-Typ > ( Ausdruck ): Dieser Operator dient expliziten Typ-Umwandlungen, wie sie im obigen Absatz beschrieben wurden.

• const_cast < Ziel-Typ > ( Ausdruck ): Dieser Operator dient einzig und al­lein dem Hinzufügen oder Weglassen des const-Qualifizierers. Weil das Weg­lassen von const fast immer gefährlich ist und das Hinzufügen vom Übersetzer nach Bedarf selbsttätig durchgeführt wird, wird dieser Operator nur selten be­nutzt.

• dynamic_cast < Ziel-Typ > ( Ausdruck ): Dieser Operator wird nur im Zu­sammenhang mit polymorphen Zeigern und Referenzen benutzt. Da mit diesem Operator viel Unfug angestellt werden kann und er in 99% aller Fälle nicht benö­tigt wird, wird er in diesem Skript nicht weiter beschrieben.

• Ein vierter Operator, reinterpret_cast, wird nur in so speziellen Situatio­nen benötigt, dass in dem Skript nicht weiter auf ihn eingegangen wird.

Die obige Klammer-Syntax für den Cast-Operator vereinigt die Funktionalität der drei Operatoren static_cast, const_cast und reinterpret_cast, aller­dings mit den genannten Nachteilen.

Generell ist zu sagen, dass explizite Typ-Umwandlungen „von Übel“ sind: Wenn der umzuwandelnde Ausdruck nicht den richtigen Typ hat, sollten Sie in der Regel prüfen, warum dies so ist, und die Ursache dementsprechend ergründen. Meistens stellt sich heraus, dass ein Cast entweder nicht notwendig oder gefährlich ist. Es gibt nur wenige Situationen, in denen der Programmierer mehr über den Typ eines Ausdrucks „weiß“ als der Übersetzer; nur in solchen Situationen ist es angebracht, eine explizite Typ-Um­wandlung vorzunehmen. Allgemein gilt, dass die Notwendigkeit, einen Ausdruck expli­zit umzuwandeln, erheblich geringer ist als in vergleichbaren Sprachen wie C und Java; selbst in C++ ist sie im Laufe der Jahre durch das Hinzufügen weiterer, mächtiger Sprachmittel wie Kovarianz (4.6.4.1) und Schablonen (7.2) erheblich geringer gewor­den.

3.4.6 Typ-AliaseC++ bietet die Möglichkeit, für bestehende Typen (unter Umständen neue) Namen einzuführen. Dies kann nützlich sein,

(1) wenn der zu benennende Typ ein komplexer zusammengesetzter Typ ist oder

67

Typ-Aliase zur Vereinfachung und Abstraktion

Syntax macht es nicht leicht, Casts aufzuspüren

alternative Cast-Operatoren

explizite Typ-Um­wandlungen sind meistens fraglich

Page 76: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

(2) wenn einfach durch eine weitere Indirektion eine Abstraktion eingeführt werden soll.

Betrachten wir den ersten Punkt. In dem Beispiel für Funktionstypen in Abschnitt 3.4.3.1 steht in Zeile 12:

12 int rechne (int a, int b, int op (int, int))Es macht Sinn, dem etwas komplexen Funktionstyp einen eigenen Namen zu geben. Dies führen wir mit Hilfe einer typedef-Definition durch:

12 typedef int Operation (int, int);13 int rechne (int a, int b, Operation op)

Betrachten Sie die neue Zeile 12. Durch das Schlüsselwort typedef wird der Be­zeichner Operation ein Typ-Name, und zwar für den (bis dato unbenannten) Funktionstyp int (int, int). Dieser Typ kann dann genauso wie int als Typ-Name in der Definition der Funktionsparameter verwendet werden. Wenn Sie sich das typedef in der Definition wegdenken, würde eine Deklaration der Funktion Operation übrig bleiben. Doch das typedef-Schlüsselwort besagt: „Definiere/ deklariere kein Objekt, definiere stattdessen einen Typ dieses (hypothetischen) Ob­jekts.“

Ein weiteres Beispiel: Angenommen, Sie wollen in Ihrem Programm Berechnungen anstellen, und Sie sind sich noch nicht ganz sicher, welchen Datentyp Sie wählen sol­len. Am einfachsten gehen Sie vor, indem Sie zuerst eine (vorläufige) Wahl treffen und via typedef einen Typ-Alias auf den gewählten Typ definieren. Diesen Alias verwenden Sie dann im gesamten Rest des Programms:

1 /*** Beispiel funktionstyp2.cpp ***/2 #include <ostream>3 #include <iostream>4 using namespace std;56 // wir wählen zuerst int als Grundlage unserer Berechnungen7 typedef int Zahl;8 // alle Berechnungen finden nun mit dem Typ Zahl statt9

10 // addiert beide Operanden und gibt das Ergebnis zurück11 Zahl add (Zahl a, Zahl b) {return a + b;}12 // subtrahiert beide Operanden voneinander und gibt das Ergebnis zurück13 Zahl sub (Zahl a, Zahl b) {return a - b;}14 // führt eine Berechnung mit beiden Operanden über die übergebene Funktion durch;15 // die Funktion muss den Typ Operation besitzen16 typedef Zahl Operation (Zahl, Zahl);17 Zahl rechne (Zahl a, Zahl b, Operation op)18 {19 // rufe übergebene Funktion mit beiden Operanden auf20 return op (a, b);21 }2223 int main ()24 {25 // führe ein paar Berechnungen durch26 cout << "2 + 5 = " << rechne(2, 5, add) << endl;27 cout << "3 - 4 = " << rechne(3, 4, sub) << endl;28 return 0;

68

Page 77: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

29 }

Wenn Sie nun den Datentyp beispielsweise in long ändern möchten, müssen Sie dies nur einmal tun, nämlich in Zeile 7. Da alle anderen Deklarationen auf diesem Typ aufbauen, sind keine weiteren Änderungen notwendig. Dies zeigt hoffentlich den Sinn einer solchen Typ-Abstraktion.

Merksatz 10: Verwende Abstraktionen!

Der typedef-Spezifizierer erstellt keine neuen Typen – er erzeugt nur neue Namen für bestehende Typen. Das bedeutet, dass Typ-Vergleiche vom Übersetzer immer auf den zugrunde liegenden Datentypen durchgeführt werden und die durch typedef ein­geführten Abstraktionen ignoriert werden. Im folgenden Code-Abschnitt sind für den Übersetzer die Datentypen Alter und Gewicht völlig identisch, obwohl sie von der Bedeutung her zwei völlig unterschiedliche Typen sind und auch im Programm unter­schiedliche Namen tragen.

1 typedef int Alter; // Typ für das Alter von Personen2 typedef int Gewicht; // Typ für das Gewicht von Personen

Die einzige Möglichkeit in C++, wirklich neue und von anderen Typen unterscheidbare Typen zu erschaffen, ist das Definieren von Klassen (oder Aufzählungen, s. 3.4.3.6). Klassen werden in Kapitel 4 eingeführt.

3.5 AnweisungenAnweisungen sind das Herz, der Motor eines jeden C++-Programms. Ohne Anwei­sungen „läuft“ im wahrsten Sinne des Wortes nichts. Vieles, was Sie bisher kennen gelernt haben – Funktionen, Typen – sind nur dazu da, um für Anweisungen ein an­genehmes Milieu zu schaffen.

In C++ wird jede Anweisung durch ein Semikolon (;) abgeschlossen, mit Ausnahme der Block-Anweisung (3.5.6). Anweisungen müssen immer innerhalb einer Funktion oder Methode existieren – wenn der Übersetzer sie außerhalb vorfindet, wird er me­ckern. Aber diese Einschränkung ist keine, denn wie Sie wissen, fängt die Ausfüh­rung eines jeden C++-Programms mit dem Aufruf der Funktion main an, und diese kann weitere Funktionen aufrufen. Sie müssen also „Ihre“ Anweisungen nur in die Funktion main oder eine der von ihr aufgerufenen Funktionen bzw. Methoden schreiben.

Generell werden Anweisungen sequentiell abgearbeitet, d. h. nacheinander: Die Aus­führung einer Anweisung wird erst begonnen, wenn die vorherige Anweisung kom­plett abgearbeitet worden ist. Es gibt aber auch Ausnahmen von der Regel, beispiels­weise bei Funktionsaufrufen (3.6.2) oder beim Werfen von Ausnahmen (5.2).

Bevor Sie allerdings blindlings irgendwelche Anweisungen in irgendwelche Funktio­nen schreiben, sollten Sie sich mit ihnen vertraut machen. Es gibt sechs wichtige Klassen von Anweisungen:

(1) Ausdrücke (3.5.1)

(2) Deklarationen (3.5.2)

(3) Fallunterscheidung und Selektion (3.5.3)

69

Typ-Aliase sind „nur“ Aliase

Anweisungen sind wichtig

Vorkommen von Anweisungen

Abarbeitung von Anweisungen

Arten von Anwei­sungen

Page 78: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

(4) Schleifen (3.5.4)

(5) Sprünge (3.5.5)

(6) Blöcke (3.5.6)

Diese werden in den nächsten Abschnitten vorgestellt. Zuvor sollen Sie jedoch die allereinfachste Anweisung in C++ kennen lernen: die Null- oder leere Anweisung:

1 ; // die „berühmte“ Null-Anweisung

Sie besteht aus einem einfachen Semikolon und tut nichts. Sie hat sozusagen „Null“ Effekt – deshalb ihr Name. Ja, auch sie hat ihre Daseinsberechtigung, allerdings nur bei bestimmten Anwendungen von Sprüngen, die dieses Skript nicht erklärt (und auch nicht erklären will). Sie sollten die Null-Anweisung jedoch kennen, weil sie für Sie eine Erleichterung bedeutet – falls Sie beim Tippen einmal versehentlich zwei Semikola hintereinander eingeben, macht dies nämlich (meistens) nichts...

3.5.1 AusdrückeEine Ausdrucks-Anweisung besteht aus lediglich einem Ausdruck (3.4), der zur Aus­führungszeit ausgewertet wird. Wenn der Ausdruck keine Seiteneffekte hat, hat die Anweisung keinerlei Effekt.

Die häufigsten Ausdrucks-Anweisungen, da mit Seiteneffekten verbunden, sind Zu­weisungen (3.4.1) und Prozeduraufrufe (3.6.2).

3.5.2 DeklarationenDeklarationen und Definitionen können überall dort stehen, wo Anweisungen erlaubt sind. Dies unterscheidet C++ von anderen Programmiersprachen wie Pascal oder C24, welche vom Programmierer die Definition aller lokalen Variablen am Anfang eines Blockes erfordern. In C++ können lokale Variablen somit überall innerhalb einer Funktion und insbesondere dort definiert werden, wo sie gebraucht werden. Dieses Lokalitätsprinzip verbessert die Verständlichkeit des Quelltextes und vermindert Fehler durch das Verwenden derselben Variable zu verschiedenen Zwecken.

Beispiel: Anstatt1 void meineFunktion ()2 {3 int zaehler;4 // ... einige Zeilen Code, die zaehler nicht benutzen ...5 zaehler = 0;6 // ... einige Zeilen Code, die zaehler benutzen ...7 }

zu schreiben, sollten Sie folgenden Programm-Code wählen1 void meineFunktion ()2 {3 // ... einige Zeilen Code, die zaehler nicht benutzen ...

24) Dies gilt nur für den verbreiteten, aber formal inzwischen überholten ISO/IEC 9899:1990-C-Stan­dard (sowie für traditionelles oder Kernighan&Ritchie-C). Der aktuelle C-Standard (ISO/IEC 9899:1999), der sich allerdings noch nicht flächendeckend durchgesetzt hat, erlaubt, dass Deklara­tionen und Anweisungen innerhalb von Blöcken in beliebiger Reihenfolge auftreten können.

70

Null-Anweisung

Ausdrücke als Anweisungen

Deklarationen als Anweisungen

Page 79: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

4 int zaehler = 0;5 // ... einige Zeilen Code, die zaehler benutzen ...6 }

Dieser Programmier-Stil vermeidet zusätzlich nicht initialisierte Variablen (Merksatz 5)! Also gleich zwei Fliegen mit einer Klappe geschlagen...

3.5.3 Fallunterscheidung und Selektion

3.5.3.1 FallunterscheidungIn jeder imperativen25 Programmiersprache wird eine Anweisung zur Fallunterschei­dung benötigt. In C++ hat sie folgenden Aufbau:

if ( Bedingung )ThenAnweisung

[elseElseAnweisung]

Die Bedeutung ist wie folgt: Zuerst wird die Bedingung zwischen den beiden runden (obligatorischen!) Klammern hinter dem if ausgewertet. Dieser Ausdruck muss da­bei vom Typ bool sein oder sich in diesen Typ konvertieren lassen (3.4.5). Wenn die Auswertung true ergibt, wird ThenAnweisung ausgeführt. Wenn die Auswer­tung false ergibt und der optionale else-Teil vorhanden ist, wird ElseAnweisung ausgeführt. Wenn die Auswertung false ergibt und kein else-Teil vorhanden ist, passiert überhaupt nichts weiter.

Beispiel:1 /*** Beispiel if2.cpp ***/2 #include <istream>3 #include <ostream>4 #include <iostream>5 using namespace std;67 /*8 * Diese Funktion prüft die Zugangsberechtigung des Benutzers9 * und liefert true bei Erfolg und false bei Misserfolg zurück,10 * abhängig vom übergebenen Alter.11 */12 bool pruefeZugangsberechtigung (int alter)13 {14 if (alter < 18)15 return false;16 else17 return true;18 }1920 /*21 * Diese Funktion darf nur bei erfolgreicher Prüfung der22 * Zugangsberechtigung aufgerufen werden!23 */24 void geschuetzterProgrammteil ()

25) d. h. Befehls- oder Anweisungs-orientierten

71

die if-Anwei­sung

Page 80: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

25 {26 // ... tut irgendetwas ...27 }2829 int main ()30 {31 cout << "Bitte geben Sie Ihr Alter ein: ";32 int alter;33 cin >> alter;34 if (!pruefeZugangsberechtigung (alter))35 cout << "Du bist leider zu jung..." << endl;36 else37 {38 cout << "Sie sind zum Zugang berechtigt." << endl;39 geschuetzterProgrammteil ();40 }41 return 0;42 }

In diesem Beispiel haben wir zwei if-Anweisungen: Einmal in der Funktion prue­feZugangsberechtigung in den Zeilen 14-17 und einmal in der Funktion main in den Zeilen 34-40, beide Male inklusive else-Teil. Bei der zweiten if-An­weisung können Sie im else-Teil sehen, dass Sie Blöcke (3.5.6) verwenden müs­sen, wenn Sie mehr als eine Anweisung im if- oder else-Teil ausführen wollen.

Erfahrene (oder aufmerksame) Leser wissen, dass die vorgestellte Syntax mehrdeutig ist. Schauen Sie sich die folgende Anweisung an:

1 if (alter >= 18)2 if (gewicht < 70)3 return true;4 else5 return false;

Ist dies nun1 if (alter >= 18)2 {3 if (gewicht < 70)4 return true;5 else6 return false;7 }

mit ThenAnweisung = „if (gewicht < 70) return true; else return false“ und ohne ElseAnweisung, oder

1 if (alter >= 18)2 {3 if (gewicht < 70)4 return true;5 }6 else7 return false;

mit ThenAnweisung = „if (gewicht < 70) return true“ und ElseAnwei­sung = „return false“? Beides ist möglich; in C++ wird die erste Möglichkeit ge­wählt. Ein else-Teil gehört also zur allernächsten if-Anweisung.

Sie können das Risiko einer Missdeutung gänzlich vermeiden, wenn Sie nur Blöcke als ThenAnweisung und ElseAnweisung verwenden.

72

Mehrdeutigkeiten

Page 81: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

Sie können als Bedingung auch eine Deklaration verwenden, vorausgesetzt dass das deklarierte Objekt sich in einen Wahrheitswert konvertieren (3.4.5) lässt. Beispiel:

1 using namespace std;2 // gib die Anzahl aus wenn ungleich Null3 if (const int anzahl = anzahlElementeInListe ())4 {5 cout << "Anzahl: " << anzahl << endl;6 }7 // ab hier kann auf die Konstante anzahl nicht mehr zugegriffen werden

Der Gültigkeitsbereich für das in der Bedingung deklarierte Objekt ist die if-An­weisung; außerhalb dessen ist das Objekt nicht existent.

VC++-Benutzer aufgepasst: In älteren Versionen des Übersetzers (einschließlich der Version 6.0, mit der wir in diesem Skript arbeiten) ist diese Einschränkung des Gültig­keitsbereichs nicht implementiert! Dort ist jede Entität, die Sie in einer if-Bedingung deklarieren, bis zum Ende des enthaltenden Blockes gültig (reicht also über das Ende der if-Anweisung hinaus). Falls Sie mit solchen Übersetzern arbeiten (müssen), emp­fiehlt es sich, alle in Bedingungen deklarierten Variablen innerhalb einer Funktion ein­deutig zu benennen, um eventuelle Überschneidungen von Gültigkeitsbereichen im Vor­feld zu vermeiden.

3.5.3.2 SelektionIn C++ gibt es auch eine Selektions-Anweisung mit folgendem Aufbau:

switch ( Bedingung ){case KonstanterAusdruck1 :

[Anweisung1Block1Anweisung2Block1...break;]

case KonstanterAusdruck2 :[Anweisung1Block2Anweisung2Block2...break;]

...[default :

[Anweisung1BlockDefaultAnweisung2BlockDefault...

73

die switch-An­weisung

Page 82: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

break;]]}

Bei der Ausführung wird zuerst Bedingung ausgewertet. Gibt es innerhalb der switch-Anweisung eine case-Marke mit demselben Wert, werden die Anweisun­gen hinter dieser Marke ausgeführt, und zwar bis eine break-Anweisung (3.5.4) ge­funden oder die switch-Anweisung beendet wird. Wenn keine passende Marke existiert, werden die Anweisungen hinter der default-Marke ausgeführt; fehlt auch diese, passiert überhaupt nichts. Die Bedingung kann wie bei der if-Anwei­sung auch eine passende Definition sein.

Ein Beispiel für die Selektions-Anweisung können Sie im Abschnitt 3.4.3.6, „Auf­zählungen“finden.

Sie müssen darauf achten, am Ende jedes „Blocks“ von Anweisungen hinter einer case-Marke eine break-Anweisung zu setzen. Andernfalls werden die Anweisungen der nächsten Marke mit ausgeführt! Das unterscheidet C++ von anderen Programmier­sprachen wie Pascal. (Manchmal ist dieses Verhalten aber auch erwünscht, siehe hierzu ebenfalls das o. g. Beispiel.)

Die switch-Anweisung ist nichts weiter als eine etwas übersichtlichere Form der if-Anweisung, allerdings mit geringerem Funktionsumfang, da die Ausdrücke in den case-Marken konstant sein müssen. if-Anweisungen haben diese Einschränkung nicht, so dass Sie jede switch-Anweisung in eine äquivalente if-Anweisung mit mehreren Zweigen (d. h. if- und else-Teilen) überführen können, etwa so:

1 if (Bedingung == KonstanterAusdruck1)2 {3 Anweisung1Block14 Anweisung2Block15 ...6 }7 else if (Bedingung == KonstanterAusdruck2)8 {9 Anweisung1Block2

10 Anweisung2Block211 ...12 }13 else if ...14 else15 {16 Anweisung1BlockDefault17 Anweisung2BlockDefault18 ...19 }

3.5.4 SchleifenSchleifen dienen dazu, Programmteile mehrfach auszuführen. In C++ gibt es deren drei:

(1) die while-Schleife als Kopfschleife

(2) die do-Schleife als Fußschleife

(3) die for-Schleife als Zählschleife

74

Vergessen Sie die break-Anwei­sungen nicht!

Schleifen in C++

if und switch im Vergleich

Page 83: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

Jede der Schleifen hat einen Schleifen-Körper und einen Schleifen-Kopf (oder Schleifen-Fuß bei der do-Schleife). Der Schleifen-Körper enthält die Anweisung, die bei jedem Schleifendurchlauf ausgeführt wird. Der Schleifen-Kopf bzw. Schleifen-Fuß enthält die kontrollierende Bedingung (s. u.)

Zu jeder Schleife gehört eine kontrollierende Bedingung, die vor oder nach jedem Schleifendurchlauf ausgewertet wird. Ist die Bedingung wahr, wird der nächste Schleifendurchlauf gestartet; ist sie falsch, wird der Schleifendurchlauf beendet. Folglich muss sich der Ausdruck stets in einen Wahrheitswert konvertieren lassen. Bei der for- und while-Schleife kann die kontrollierende Bedingung auch die De­finition einer Variable sein, deren Gültigkeitsbereich dann lokal zur Schleife ist und beim Verlassen der Schleife wieder verschwindet.

Innerhalb jeder dieser drei Schleifen können die break- und die continue-An­weisungen vorkommen. Die break-Anweisung bewirkt, dass die Ausführung der Schleife abgebrochen wird und die Anweisungen hinter dem Schleifen-Körper aus­geführt werden. Dies ähnelt der Verwendung von break innerhalb der switch-Anweisung, wo ja auch der switch-Körper verlassen wird. Die break-Anweisung ist also eine alternative (und weniger strukturierte) Möglichkeit, eine Schleife zu ver­lassen.

Die continue-Anweisung beendet vorzeitig den aktuellen Schleifendurchlauf und fängt unmittelbar den nächsten Schleifendurchlauf an. Allerdings wird bei allen drei Schleifen die kontrollierende Bedingung geprüft, bevor der nächste Schleifendurch­lauf angefangen wird.

3.5.4.1 Die while-Schleife

Die while-Schleife ist eine Kopf-gesteuerte Schleife. Das bedeutet, dass die kon­trollierende Bedingung vor jedem Schleifendurchlauf ausgewertet wird. Das kann dazu führen, dass die Schleife überhaupt nicht ausgeführt wird.

Beispiel: Betrachten Sie den folgenden Programm-Code:1 /*** Beispiel while.cpp ***/2 #include <istream>3 #include <ostream>4 #include <iostream>5 #include <string>6 using namespace std;78 /*9 * Diese Funktion konvertiert die übergebene Zahl in die oktale10 * Darstellung (also zur Basis 8). Es wird immer eine führende Null11 * vorangestellt.12 */13 string dezimalZuOktal (int zahl)14 {15 const int Basis = 8; // die verwendete Basis16 string ergebnis = ""; // akkumuliert die umgewandelten Ziffern17 while (zahl > 0)18 {19 // berechne nächste Ziffer20 const int rest = zahl % Basis;

75

kontrollierende Bedingung

die break-An­weisung

die continue-Anweisung

Aufbau von Schleifen

die while-An­weisung

Page 84: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

21 // wandle in passendes Zeichen um22 const char ziffer = '0' + rest;23 // packe die Ziffer vor das Ergebnis24 ergebnis = ziffer + ergebnis;25 // reduziere die umzuwandelnde Zahl26 zahl /= Basis;27 }28 // führende Null, damit wir im Falle zahl == 0 keine leere Zeichenkette zurückgeben29 return "0" + ergebnis;30 }3132 int main ()33 {34 cout << "Bitte geben Sie eine Dezimalzahl ein: ";35 int zahl;36 cin >> zahl;37 cout << "Die Zahl in oktaler Schreibweise lautet: ";38 cout << dezimalZuOktal (zahl) << endl;39 return 0;40 }

Dieses Programm enthält eine Funktion, die eine Dezimalzahl in die korrespondie­rende oktale Darstellung (zur Basis 8) umwandelt. Der Kern dieser Funktion ist eine while-Schleife, die solange ausgeführt wird, wie Ziffern zur Umwandlung zur Ver­fügung stehen (d. h. solange die Zahl ungleich Null ist). Die Bedingung ist formuliert als zahl > 0, damit die Funktion auch abbricht, wenn (gemeinerweise) eine nega­tive Zahl übergeben wird.

Hier wird auch deutlich, warum in diesem Beispiel eine Kopf-gesteuerte Schleife notwendig ist. Wenn eine negative Zahl übergeben wird, darf der Schleifen-Körper nicht durchlaufen werden, weil dies zu einem negativer Rest führt, der in dieser Funktion keinen Sinn macht und seltsame Effekte zur Folge hat.

3.5.4.2 Die do-Schleife

Die do-Schleife ähnelt der while-Schleife, nur dass sie Fuß-gesteuert wird. Sie wird also mindestens einmal durchlaufen. Die kontrollierende Bedingung wird nach jedem Schleifendurchlauf geprüft.

Beispiel:1 /*** Beispiel do.cpp ***/2 #include <istream>3 #include <ostream>4 #include <iostream>5 using namespace std;67 int liesAlter ()8 {9 int alter = 0;

10 do11 {12 cout << "Alter: ";13 cin >> alter;14 }15 while (alter < 0);16 return alter;17 }

76

die do-Anwei­sung

Page 85: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

1819 int main ()20 {21 const int alter = liesAlter ();22 cout << "Ihr Alter ist: " << alter << endl;23 return 0;24 }

Die Funktion liesAlter liest das Alter vom Anwender ein. Um ein Alter kleiner Null zu vermeiden, wird der Anwender so lange „malträtiert“, bis er ein korrektes Alter größer oder gleich Null eingegeben hat. Diese Prüfung wird in der kontrollie­renden Bedingung der Schleife durchgeführt. Da die Prüfung aber nur dann Sinn macht, wenn vorher das Alter einmal eingelesen worden ist, und weil das Einlesen im Schleifen-Körper stattfindet, wurde die Fuß-gesteuerte do-Schleife ausgewählt. Denn nur diese stellt sicher, dass die Schleife wie verlangt mindestens einmal ausge­führt wird.

3.5.4.3 Die for-Schleife

Die for-Schleife wird benutzt, wenn Sie von vornherein wissen, wie viele Schlei­fendurchläufe anstehen. Sie hat die folgende Syntax:

for ( Initialisierung [Bedingung]; [Iterationsausdruck] )Anweisung

Sie entspricht der folgenden Konstruktion:{

Initialisierungwhile ( Bedingung ){

AnweisungIterationsausdruck;

}}

Initialisierung ist entweder eine Ausdrucks-Anweisung (3.5.1), eine Deklarations-Anweisung (4.4.5.2) oder die leere Anweisung (3.5). Sie wird häufig dazu verwen­det, einen Schleifenzähler (auch Laufvariable genannt) mit dem Startwert zu initiali­sieren. Falls Sie die Laufvariable in Initialisierung definieren, ist sie nur innerhalb der for-Schleife verfügbar; sobald die Schleife verlassen wird, ist sie nicht mehr existent.

Da eine Anweisung immer mit einem Semikolon (;) endet, enthält eine for-Anwei­sung zwischen den beiden runden Klammern immer zwei Semikola, auch wenn alle op­tionalen Teile weggelassen werden.

77

die for-Anwei­sung

Page 86: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

In VC++ ist die Beschränkung des Gültigkeitsbereichs der Laufvariable einer for-Schleife nicht implementiert! Wenn Sie VC++ verwenden (müssen) und Laufvariablen in Schleifen-Kopf initialisieren, müssen Sie darauf achten, dass diese Variablen auch außerhalb der Schleife gültig bleiben und dort keine Objekte mit gleichem Namen defi­niert werden:

1 // Beispiel-Schleife ohne irgendeine Funktion2 for (int i = 0; i < 10; ++i) { }3 int i = 42; // Fehler in VC++: i noch im Gültigkeitsbereich

Bedingung wird vor jedem Schleifendurchlauf (auch vor dem ersten) zu einem Wahr­heitswert ausgewertet (d. h. der Ausdruck muss vom Typ bool sein oder sich in die­sen Typ konvertieren lassen, s. Abschnitt 3.4.5). Nur wenn die Auswertung true er­gibt, wird der nächste Schleifendurchlauf durchgeführt, ansonsten wird die Schleife abgebrochen. Sie werden in dem Ausdruck zum Testen der Abbruchbedingung fast immer auf den Schleifenzähler zugreifen wollen, den Sie in Initialisierung gesetzt haben.

Iterationsausdruck wird nach jedem erfolgten Schleifendurchlauf ausgewertet. Sie werden an dieser Stelle immer einen Ausdruck einsetzen, der den Schleifenzähler er­höht, erniedrigt oder auf einen anderen neuen Wert setzt.

Schließlich stellt Anweisung jene Anweisung dar, die bei jedem Schleifendurchlauf ausgeführt werden soll.

Beispiel 1: Diese Schleife gibt die Zahlen zwischen Eins und Zehn (beide einge­schlossen) aus und wird folglich zehnmal durchlaufen.

1 /*** Beispiel for1.cpp ***/2 #include <ostream>3 #include <iostream>4 using namespace std;56 int main ()7 {8 for (int zaehler = 1; zaehler <= 10; ++zaehler)9 cout << zaehler << endl;

10 return 0;11 }

Beispiel 2: Diese Schleife zählt von 60 in Fünfer-Schritten herunter, bis die Laufvari­able Null oder negativ wird. Sie wird somit zwölfmal durchlaufen.

1 /*** Beispiel for2.cpp ***/2 #include <ostream>3 #include <iostream>4 using namespace std;56 void tuEtwas ()7 {8 cout << "..." << endl;9 }

1011 int main ()12 {13 for (int minuten = 60; minuten > 0; minuten -= 5)14 {15 cout << "Noch " << minuten << " Minuten!" << endl;16 tuEtwas ();

78

Page 87: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

17 }18 return 0;19 }

Hier sehen Sie auch, wie Sie mehrere Anweisungen in den Schleifen-Körper schrei­ben können, nämlich indem Sie sie in einem Anweisungsblock (3.5.6) unterbringen.

Wenn Sie die continue-Anweisung innerhalb einer for-Schleife verwenden, wer­tet diese erst den Iterationsausdruck aus, bevor sie die Abbruchbedingung überprüft.

Der kundige Leser wird einwenden, dass die for-Schleife nicht nur zum Zählen ver­wendet werden kann. Das ist richtig, Sie sollten es sich aber angewöhnen, für alles an­dere die while- oder do-Schleifen zu verwenden. Die hat zum einen historische Grün­de (for ist nun mal die Zählschleife), zum anderen verwirren Sie nur die Leute, die Ihren Code lesen (Sie selbst eingeschlossen).

3.5.5 SprüngeC++ kennt auch einige Anweisungen, um den Programmablauf ohne zusätzliche Be­dingungen zu verändern. Wir stellen hier nur die return-Anweisung vor; die break- und continue-Anweisungen wurden bereits bei den Schleifen und der Se­lektions-Anweisung vorgestellt, und die unstrukturierte goto-Anweisung ist der Schrecken aller Informatiker...26

Die return-Anweisung hat zwei Varianten:

(1) return Ausdruck;(2) return;Die erste Variante kommt nur in „echten“ Funktionen vor, also in Funktionen, die ei­nen Wert zurückgeben. Dort ist sie auch zwingend vorgesehen und ermöglicht es, dem Aufrufer der Funktion ein Ergebnis zu liefern, das zum Rückgabetyp der Funk­tion Typ-verträglich ist. Fehlt in einer solchen Funktion die return-Anweisung, liegt ein Fehler vor.

Die zweite Variante ist nur in Prozeduren (also void-Funktionen) erlaubt, dort op­tional und springt beim Erreichen zum Aufrufer zurück. Wenn innerhalb einer Proze­dur keine return-Anweisung steht, wird die Kontrolle nach dem Ausführen der letzten Anweisung an den Aufrufer zurückgegeben, d. h. so, als ob vor dem abschlie­ßenden } ein return; stünde.

In beiden Fällen wird Programm-Code, der zwar innerhalb der Funktion aber hinter der return-Anweisung steht, nicht ausgeführt, da die Kontrolle an den Aufrufer zurück­gegeben wird. Sie sollten darauf achten, dass kein toter Code entsteht, d. h. Anweisun­gen, die nie erreicht und ausgeführt werden.

Anders als in Java meckert der Übersetzer toten Code (s. o.) nicht als Fehler an.27 Sie sind also ziemlich auf sich alleine gestellt. Um die Gefahr von totem Code zu vermin­dern, empfiehlt es sich, nur eine einzige return-Anweisung innerhalb einer Funktion zu benutzen und diese ans Ende der Funktion zu stellen. So stellen Sie sicher, dass alle möglichen Pfade durch die Funktion letztlich an der return-Anweisung „vorbeikom­men“.

26) vgl. [Dijk68]27) Ein guter Übersetzer wird aber sicherlich eine deutliche Warnung ausgeben.

79

Sprung-Anwei­sungen in C++

die return-An­weisung

die return-An­weisung in Funk­tionen

die return-An­weisung in Proze­duren

toten Code ver­meiden

Page 88: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

3.5.6 BlöckeBlöcke sind Hüllen um Anweisungen. Sie erscheinen nach außen wie eine einzige Anweisung, können aber aus vielen Anweisungen bestehen. Sie sind für all die Fälle gedacht, in denen C++ nur eine einzige Anweisung erlaubt, Sie aber mehr als eine Anweisung verwenden wollen. Dies ist der Fall:

• bei der if-else-Fallunterscheidung im if- und else-Teil

• bei der for-Schleife

• bei der while-Schleife

• bei der do-Schleife

Beispiele für Blöcke haben Sie bereits in den letzten Abschnitten zuhauf kennen ge­lernt.

Wichtig ist, dass jeder Block einen eigenen Gültigkeitsbereich für Variablen, Kon­stanten etc. darstellt. Objekte, die Sie in einem Block deklarieren, sind außerhalb des Blockes weder verfügbar noch existent. Zusätzlich können Objekte in inneren Blö­cken Objekte in äußeren Blöcken verdecken (3.3.5). Beispiel:

1 bool pruefeBerechtigung (int alter, int gewicht, int groesse)2 {3 if (alter >= 18)4 {5 // berechne den BMI (Body Mass Index)6 int bmi = gewicht * 10000 / (groesse*groesse);7 if (bmi > 25)8 {9 return false; // zu dick!

10 }11 else12 {13 return true; // alles OK14 }15 }16 else17 {18 // hier ist bmi nicht existent19 return false; // zu jung!20 }21 // hier ist bmi ebenfalls nicht existent22 }

Die lokale Variable bmi, die in Zeile 6 definiert wird, ist außerhalb des zugehörigen Blockes (Zeilen 4-15) nicht existent, wohl aber innerhalb innerer Blöcke (wie z. B. in Zeile 9 und 13).

3.6 FunktionenFunktionen bilden die Abstraktion in der Informatik für auszuführende Aufgaben. Sie kapseln Anweisungen und besitzen eine Schnittstelle, welche die benötigten Da­ten, die zurückgegebenen Ergebnisse sowie die Aufgabe der Funktion definiert. Be­vor wir jedoch detaillierter auf Funktionen eingehen, ein kleiner Hinweis: Alles, was in diesem Abschnitt über Funktionen gesagt wird, ist in dem gleichen Maße für Me­

80

Gruppieren von Anweisungen

jeder Block ist ein Gültigkeitsbe­reich

Charakter von Funktionen

Page 89: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

thoden von Klassen (4.4.5.1) von Bedeutung, außer dass die Syntax sich etwas unter­scheidet.

Sie haben in dem Skript bisher sehr viele Beispiele für Funktionen kennen gelernt und sind mit der Syntax von Funktionen einigermaßen vertraut. Dieser Abschnitt geht deshalb weniger auf die Syntax als auf die Verwendung von Funktionen ein und erläutert, worauf beim Definieren und Verwenden von Funktionen zu achten ist.

Eines ist ganz wichtig, egal ob es um Funktionen, Methoden, Operationen oder worum auch immer geht: der Unterschied zwischen Definition auf der einen Seite und Anwendung oder Applikation auf der anderen Seite. Zuerst wird irgendwo im Programm eine Funktion definiert, d. h. es werden der Name, eventuelle Parameter und der Rückgabetyp festgelegt sowie die Semantik der Funktion (= Bedeutung) über die enthaltenen Anweisungen beschrieben. Nun kann an einer beliebigen Stelle im Programm diese Funktion aufgerufen werden. Das heißt, dass die Funktion während des Progammlaufs als Dienstleister angesehen wird, deren Dienst man nutzt.

Die Unterschiede zwischen Definition und Anwendung einer Funktion sind enorm:

(1) Sie definieren eine Funktion genau einmal, können Sie aber ganz oft aufrufen (oder sogar gar nicht!)

(2) Definition und Anwendung einer Funktion können sich in ganz unterschiedli­chen Programmteilen befinden. (Für den Aufruf einer Funktion ist allerdings zu­mindest eine Deklaration notwendig, damit der Übersetzer weiß, dass er es mit einer Funktion zu tun hat.)

(3) Die Definition einer Funktion geschieht zur Übersetzungszeit, ihre eventuellen Aufrufe erst zur Laufzeit.

(4) Der letzte Punkt impliziert, dass Sie sehr wohl von vornherein sagen können, welche Funktionen in Ihrem Programm existieren, aber nicht, welche Funktionen bei einem Testlauf ausgeführt werden, denn dies könnte von Eingaben des An­wenders abhängen.

3.6.1 Das prozedurale ParadigmaFunktionen und Prozeduren sind die Umsetzung von Algorithmen. Das prozedurale Paradigma sieht in Funktionen und Prozeduren das zentrale Mittel, um Programme zu strukturieren. Nach dieser Sichtweise ist die Hauptarbeit des Programmierers,

(1) zu entscheiden, welche Funktionen und Prozeduren gebraucht werden,

(2) den besten Algorithmus für das vorliegende Problem zu implementieren.

Dieses Paradigma sieht also den Algorithmus, d. h. das Verhalten, als besonders wichtig an. Daten sind von untergeordneter Bedeutung und werden in den Prozess der Programm-Strukturierung nicht oder nur ungenügend herangezogen. Die getrenn­te Betrachtung von Verhalten und Daten führt oft dazu, dass die Daten-Abhängigkei­ten zwischen den einzelnen Programm-Teilen nur schwer zu erkennen sind. Das wie­derum führt letztlich zu unübersichtlichem Code, da bei der Programm-Wartung, der Erweiterung um neue Funktionalität oder bei der Fehler-Beseitigung schnell mal ein

81

Unterschied zwi­schen Definition und Anwendung

das prozedurale Paradigma

Daten sind unter­geordnet

Page 90: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

Parameter oder eine globale Variable hinzugefügt wird, ohne sich Gedanken darüber zu machen, wo diese Daten am besten hingehören.

Diesen Nachteil beseitigt das objektorientierte Paradigma, das in Kapitel 4 vorge­stellt wird. Die objektorientierte Sichtweise betrachtet Daten und Verhalten als eine Einheit und versucht, Programme und Systeme auf der Grundlage dieser Einsicht zu strukturieren. Das Ergebnis sind Programme, in denen die Verteilung von Daten und Verhalten klar zu erkennen ist. Das ist besonders dann vorteilhaft, wenn das Pro­gramm geändert oder erweitert werden muss.

Trotz der offensichtlichen Nachteile der prozeduralen Sichtweise wollen wir Funk­tionen als Mittel zur Strukturierung von C++-Programmen vorstellen, nicht zuletzt deswegen, weil Funktionen und Methoden (die objektorientierte Variante von Funk­tionen) sich relativ ähnlich sind. In den nächsten Abschnitten gehen wir deshalb nä­her auf die unterschiedlichen Formen von Funktionen in C++ ein.

3.6.2 (Klassische) FunktionenIn der Mathematik berechnen Funktionen aus einem oder mehreren Werten einen neuen Wert, wobei die Funktionsdefinition die zugehörige Rechenregel umfasst. In der Informatik geht es nicht immer ums Rechnen, aber die Analogie ist passend. Charakteristisch für Funktionen (auch in der Informatik) ist die Rückgabe eines Er­gebnisses. Ob das Ergebnis innerhalb der Funktion berechnet wird, von anderen Funktionen geliefert wird oder konstant ist, spielt dabei keine Rolle.

Funktionen können so aufgebaut sein, dass sie auf Daten operieren, die beim Aufruf an die Funktion übergeben werden. Diese Daten heißen Argumente, die die Argu­mente aufnehmenden Objekte Parameter. Eine Funktion in der Mathematik hat im­mer mindestens ein Argument; in der Informatik muss das nicht so sein, denn eine Funktion kann einen konstanten Wert zurückliefern, wobei in diesem Fall kein Argu­ment benötigt wird.28 Das folgende Beispiel zeigt zwei Funktionen, eine ohne und eine mit Parametern:

1 // liefere den Umrechnungskurs von Euro zur (alten) D-Mark2 double gibKursEURzuDEM ()3 {4 return 1.95583;5 }67 // rechne einen Betrag von Euro in D-Mark um (ohne auf 5 Nachkommastellen zu runden)8 double wandleEURzuDEM (double betragInEUR)9 {

10 return betragInEUR * gibKursEURzuDEM ();11 }

Da Funktionen immer einen Wert zurückgeben müssen, enthalten Sie immer eine return-Anweisung zur Übermittlung des Wertes an den Aufrufer. Dieser Wert muss zum Rückgabetyp der Funktion passen (3.4.5).

28) De facto ist eine solche Funktion ohne Parameter von einer Konstante semantisch nicht zu unter­scheiden. Lediglich die Syntax erfordert ein zusätzliches Paar Klammern bei der Applikation einer Funktion verglichen mit der Applikation einer gewöhnlichen Konstanten.

82

klassische Funk­tionen

Argumente und Parameter

die return-An­weisung

OO betrachtet Einheit von Daten und Verhalten

Page 91: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

Der Name, der Rückgabetyp und die Parameter bilden zusammen mit dem Funk­tionskommentar die Schnittstelle der Funktion, die Anweisungen in der Funktions­definition sind die Implementierung der Funktion. Die Schnittstelle gibt an, was die Funktion tut, welche Eingaben sie erwartet und welche Ausgaben sie verspricht. Die Implementierung drückt das Wie der Funktion durch C++-Code aus. Die Schnittstelle muss so beschaffen sein, dass sie zum Verständnis und Benutzung der Funktion aus­reichend ist. Fehlt diese Eigenschaft, so kann die Funktion eigentlich nicht genutzt werden und wird überflüssig. Nicht immer kann nämlich die Implementierung heran­gezogen werden, um herauszufinden, was die Funktion wirklich tut:

• Der Quelltext ist nicht verfügbar: Eventuell besitzen Sie den Quelltext der Funktion nicht. Es gibt proprietäre Bibliotheken, die nur im übersetzten Format ausgeliefert werden und denen nur Header-Dateien mit den entsprechenden Schnittstellen beigelegt sind.

• Der Quelltext ist in einer unbekannten Programmiersprache verfasst: Manchmal gibt es eine C++-Schnittstelle für eine Funktion, die in einer anderen Programmiersprache implementiert ist. Wenn Sie diese Sprache jedoch nicht ver­stehen, können Sie auch den Sinn und Zweck der Funktion nicht verstehen.

• Der Quelltext ist unübersichtlich oder komplex: Wenn die Funktion einen komplexen Algorithmus implementiert oder aus viel Code besteht, kann es sein, dass Sie die Funktionsweise trotz des Quelltextes nicht verstehen. Man sieht in einem solchen Fall sozusagen den Wald vor lauter Bäumen nicht.

Um all diese Probleme aus dem Weg zu schaffen, sollten Sie ausführliche Funktions­kommentare zu den Schnittstellen der Funktionen schreiben, welche die Aufgabe der Funktion genau erläutern. Die Kommentare sollten sich auf das Was beschränken; Angaben zur Implementierung sollten vermieden oder nur sehr sparsam gemacht werden, um die Implementierung austauschbar zu halten.

Merksatz 11: Verwende ausführliche Funktionskommentare!

Eine Funktion wird aufgerufen, indem ihr Name in einem Ausdruck erscheint, ge­folgt von einer öffnenden und schließenden runden Klammer, zwischen denen sich eventuell benötigte Argumente (3.6.4) befinden. Beim Aufruf werden die Argumente in die zugehörigen Parameter (3.6.4) kopiert und die Ausführung der Anweisungen innerhalb der Funktion begonnen.

Zum Abschluss wollen wir den syntaktischen Aufbau von Funktionen betrachten:

Rückgabetyp Name ( [Parameterliste] ){

[Anweisungen]}

Dabei ist Rückgabetyp ungleich void, und in Anweisungen ist mindestens eine re­turn-Anweisung mit einem zum Rückgabetyp passenden Ausdruck enthalten.

83

Aufruf von Funk­tionen

Schnittstelle unf Implementierung

Probleme bei schlechter Schnittstellen-Do­kumentation

Funktionskom­mentare

Page 92: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

Die betrachteten Funktionen liefern immer nur einen Wert zurück. Wollen Sie mehr als einen Wert zurückliefern, ist es besser, Prozeduren (3.6.3) mit Ausgabe-Parame­tern (3.6.4) zu verwenden.

3.6.3 Prozeduren oder Funktionen „ohne Wert“Prozeduren sind wie Funktionen, nur dass sie keinen Wert zurückliefern. Stattdessen führen sie Seiteneffekte durch, indem sie beispielsweise andere Prozeduren aufrufen oder Ausgabe-Parametern (3.6.4) einen Wert zuweisen. Sie werden genauso definiert wie „normale“ Funktionen, nur dass sie immer den Rückgabetyp void besitzen und nicht unbedingt eine return-Anweisung enthalten müssen. Das folgende Beispiel stellt eine Prozedur vor, welche die Funktionen aus dem vorherigen Abschnitt nutzt, um solange Euro-Beträge in D-Mark umzurechnen, bis der Anwender „aufgibt“:

1 using namespace std;2 /*3 * Diese Prozedur liest solange Euro-Beträge vom Benutzer ein und4 * wandelt sie in D-Mark um, bis eine Null eingegeben wird.5 */6 void rechneUm ()7 {8 while (true) // Achtung: Abbruch durch return innerhalb der Schleife!9 {

10 cout << "Euro-Betrag eingeben (0 = Ende): ";11 double betrag;12 cin >> betrag;13 if (betrag == 0.0)14 return; // verlasse Schleife15 else16 {17 double ergebnis = wandleEURzuDEM (betrag);18 cout << "Ergebnis: " << ergebnis << " DEM" << endl;19 }20 }21 }

3.6.4 Parameter und ArgumenteParameter und Argumente ermöglichen es, Funktionen in ihrem Verhalten unmittel­bar zu beeinflussen. Auf der Seite der Funktionsdefinition werden Parameter wie normale Variablen-Definitionen zwischen die runden Klammern nach dem Funk­tionsnamen geschrieben, wobei sie voneinander durch Kommata abgetrennt werden:

Rückgabetyp Name ( [Typ1 Parameter1 [, Typ2 Parameter2 [, ...]]] )Zur Laufzeit muss bei jedem Aufruf dieser Funktion jeder Parameter mit einem pas­senden Wert „belegt“ werden. Der Übersetzer achtet penibel genau darauf, dass die Anzahl der Argumente beim Aufruf genau der Anzahl der Parameter in der Funk­tionsdefinition ist. Außerdem überprüft er, ob sich jedes Argument in den Typ des zugehörigen Parameters umwandeln lässt (3.4.5). „Zugehörig“ bedeutet dabei, dass das erste Argument dem ersten Parameter zugeordnet wird, das zweite Argument dem zweiten Parameter u. s. w.

84

mehrere Ergeb­nisse

Prozeduren vs. Funktionen

Parameter und Argumente

Zuordnung von Argumenten zu Parametern

Page 93: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

Die Reihenfolge der Auswertung der Argumente beim Aufruf einer Funktion ist nicht spezifiziert. So ist es im unten angegebenen Beispiel unklar, welche Ausgabe das Pro­gramm erzeugt:

1 /*** Beispiel aufruf.cpp ***/2 #include <ostream>3 #include <iostream>4 #include <string>5 using namespace std;67 // gibt die verwendete Programmiersprache zurück8 string gibSprache ()9 {10 cout << "Sprache" << endl;11 return "C++";12 }13 // bewertet die verwendete Programmiersprache14 string gibBewertung ()15 {16 cout << "Bewertung" << endl;17 return "toll";18 }19 // gibt die Sprache und die Bewertung aus20 void bewerte (string sprache, string bewertung)21 {22 cout << sprache << " ist " << bewertung << endl;23 }24 int main ()25 {26 bewerte (gibSprache (), gibBewertung ());27 return 0;28 }

Das Programm könnteSpracheBewertungC++ ist toll

oderBewertungSpracheC++ ist toll

ausgeben, da nicht definiert ist, ob beim Aufruf der Funktion bewerte zuerst das linke oder zuerst das rechte Argument ausgewertet wird. Sie sollten allgemein vermeiden, solchen Code zu schreiben, bei dem es innerhalb eines Funktionsaufrufs auf eine be­stimmte Reihenfolge von Seiteneffekten ankommt.

VC++ wertet beim Funktionsaufruf die Argumente von rechts nach links aus, weshalb die Ausgabe

BewertungSpracheC++ ist toll

lautet.

In Java ist die Reihenfolge der Auswertung spezifiziert (sie geschieht immer von links nach rechts, d. h. das erste Argument in der Liste wird auch als erstes ausgewertet), so dass Java-Programmierer besonders aufpassen sollten, da diese Regel in C++ nicht mehr gilt. Allerdings gilt es auch in Java als schlechter Programmierstil, Argumente mit Seiteneffekten zu verwenden.

85

Page 94: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

Der letzte Teil dieses Abschnitts behandelt Referenzen als Funktionsparameter. Nor­malerweise spielt es für den Aufrufer keine Rolle, ob die Funktion ihre Parameter verändert oder nicht, da die Veränderungen für ihn keinerlei Auswirkungen haben. Diese Parameter werden auch als Eingabe-Parameter bezeichnet, weil über sie ledig­lich Daten in die Funktion gelangen und nicht wieder heraus. Beispiel:

1 /*** Beispiel param1.cpp ***/2 #include <ostream>3 #include <iostream>4 using namespace std;56 // erhoht übergebene Variable um Eins und gibt den neuen Wert aus7 void erhoehe (int variable)8 {9 variable++;

10 cout << "Variable erhöht auf: " << variable << endl;11 }1213 int main ()14 {15 int zaehler = 1;16 cout << "Zähler ist: " << zaehler << endl;17 erhoehe (zaehler);18 cout << "Zähler ist: " << zaehler << endl;19 return 0;20 }

Wenn Sie dieses Programm laufen lassen, erzeugt es die Ausgabe:Zähler ist: 1Variable erhöht auf: 2Zähler ist: 1

Haben Sie die Ausgabe, insbesondere die letzte Zeile, erwartet? Wenn nicht, rufen Sie sich ins Gedächtnis, dass beim Aufruf der erhoehe-Funktion in Zeile 17 der Parameter variable mit dem Wert des Arguments (d. h. dem Inhalt der Variable zaehler) initialisiert wird. Der Wert des Zählers wird also in den Parameter ko­piert. Somit können Änderungen am Parameter keine Auswirkungen auf die ur­sprünglich übergebene Variable haben, weil der Parameter eine Kopie des Werts be­sitzt. Diese Art der Übergabe wird Übergabe per Wert genannt, weil nur der Wert der Variable, des Objekts etc. übergeben wird, aber nicht die Variable selbst.

Falls Ihnen das immer noch nicht klar ist, stellen Sie sich einfach vor, dass die Zeile 17 folgendermaßen lautet:

17 erhoehe (1);

Abgesehen davon, dass dieser Aufruf der Funktion erhoehe gemäß dem Funk­tionskommentar nicht sinnvoll ist (im Kommentar steht ja, dass eine Variable erhöht wird), ist dieser Aufruf vollkommen legal: Der Wert 1 wird in den Parameter va­riable kopiert. Natürlich kann die Erhöhung in Zeile 9 nicht die Konstante 1 auf den Wert 2 ändern – schließlich ist es eine Konstante. (Das Programm erzeugt auch dieselbe Ausgabe wie vorher).

Statt der Zahlen-Konstante 1 könnten Sie auch zaehler mit Hilfe von const kon­stant machen:

86

Eingabeparame­ter

Veränderungen werden nicht übernommen

Konstanten sind erlaubt

Page 95: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

15 const int zaehler = 1;Auch hierbei bleibt das Ergebnis dasselbe: Das Programm lässt sich übersetzen, die Ausgabe bleibt wie gehabt. Und hier wollen Sie auch bestimmt nicht, dass die Funk­tion erhoehe hinter Ihrem Rücken den Wert Ihrer – bis dato – Konstante zaeh­ler einfach ändert.

Was wir also brauchen, ist

(1) eine Möglichkeit, innerhalb einer Funktionsdefinition zu sagen: „Hier muss eine Variable übergeben werden, eine Konstante reicht nicht aus.“ und

(2) eine Möglichkeit, Veränderungen an solchen Parametern nach außen sichtbar zu machen.

Beides wird dadurch gelöst, dass der betreffende Parameter als Referenz definiert wird. Sie erinnern sich: Referenzen sind Verweise. Wenn der Parameter also ein Ver­weis auf die ursprünglich übergebene Variable ist, dann ist unser Problem gelöst.

Das angepasste Programm sieht nun folgendermaßen aus:1 /*** Beispiel param2.cpp ***/2 #include <ostream>3 #include <iostream>4 using namespace std;56 // erhoht übergebene Variable um Eins und gibt den neuen Wert aus7 void erhoehe (int &variable) // beachten Sie das &-Zeichen!8 {9 variable++;10 cout << "Variable erhöht auf: " << variable << endl;11 }1213 int main ()14 {15 int zaehler = 1;16 cout << "Zähler ist: " << zaehler << endl;17 erhoehe (zaehler);18 cout << "Zähler ist: " << zaehler << endl;19 return 0;20 }

Und voilà! Die produzierte Ausgabe beläuft sich auf:Zähler ist: 1Variable erhöht auf: 2Zähler ist: 2

Das ist genau dass, was wir erreichen wollten. Und Sie können es ruhig mal auspro­bieren, erhoehe (1) zu schreiben oder zaehler via const zu einer Konstante zu machen. Glauben Sie mir, Sie werden keinen Erfolg haben. Der Übersetzer wird Ihnen korrekterweise ankreiden, dass Sie eine veränderbare Variable an die Funktion übergeben müssen.

87

Voraussetzungen für veränderbare Parameter

Referenzen sind die Lösung

Page 96: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

Derartige Parameter, die sowohl zur Eingabe als auch zur Ausgabe von Daten be­nutzt werden, werden Ein-/Ausgabe-Parameter genannt, die Art der Übergabe wird Übergabe per Referenz genannt. Daneben gibt es auch reine Ausgabe-Parameter, de­ren Bedeutung klar sein sollte. Ausgabe-Parameter werden vor allem dann benutzt, wenn mehrere Werte zurückgeliefert werden müssen. Beispiel:

1 /*** Beispiel param3.cpp ***/2 #include <ostream>3 #include <iostream>4 using namespace std;56 // berechnet den Quotienten und Divisionsrest beider Operanden7 void quotientUndRest (8 int dividend, int divisor, int &quotient, int &rest9 )

10 {11 quotient = dividend / divisor;12 rest = dividend % divisor;13 }1415 int main ()16 {17 int quotient = 0;18 int rest = 0;19 quotientUndRest (7, 5, quotient, rest);20 cout21 << "7 / 5 = " << quotient << "\n"22 << "7 % 5 = " << rest << endl;23 return 0;24 }

Die generierte Ausgabe ist:7 / 5 = 17 % 5 = 2

3.6.5 RekursionDas Thema „Rekursion“ dreht sich um Funktionen, die – direkt oder indirekt – sich selbst aufrufen. Eine solche Funktion haben Sie bereits in dem Beispiel in Abschnitt 3.2.1 gesehen. Lassen Sie uns die enthaltenen Fehler in den Kommentaren korrigie­ren und anhand des Beispiels verstehen, wie rekursive (also sich selbst aufrufende) Funktionen funktionieren:

1 /*** Beispiel fakultaet2.cpp ***/2 #include <ostream>3 #include <iostream>4 using namespace std;56 /*7 * Diese Funktion berechnet die Fakultät.8 * Eingabe:9 * „argument“ – das Argument der Funktion

10 * Ausgabe:11 * das Ergebnis der Fakultät von „argument“12 * Bemerkung:13 * Die Fakultät fakultaet ist rekursiv definiert als:14 * fakultaet(n) = 1 [n = 0]15 * fakultaet(n) = n * fakultaet (n – 1) [n > 0]

88

Ein-/Ausgabe- und reine Ausga­be-Parameter

Funktionen, die sich selbst aufru­fen

Page 97: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

16 */17 int fakultaet (int argument)18 {19 if (argument == 0) // Prüfe, ob das Ende der Rekursion erreicht ist20 // Rekursionsende („Bottom case“) erreicht, liefere Eins zurück21 return 1;22 else23 // (n-1)! wird rekursiv ausgerechnet24 return argument * fakultaet (argument - 1);25 }2627 int main ()28 {29 cout30 << "0! = " << fakultaet (0) << "\n"31 << "1! = " << fakultaet (1) << "\n"32 << "3! = " << fakultaet (3) << "\n"33 << "6! = " << fakultaet (6) << endl;34 return 0;35 }

Die wichtige Funktion fakultaet in diesem Beispiel ist rekursiv. Warum? Weil sie sich in Zeile 24 selbst aufruft:

24 return argument * fakultaet (argument – 1);Jetzt fragen Sie sich, wieso das Programm dennoch funktionieren kann. Wenn die Funktion sich selbst aufruft, und bei diesem zweiten Aufruf sich wieder selbst auf­ruft, wobei sie bei dem jetzt dritten Aufruf sich wieder aufruft, und... dann sieht es ganz so aus, als würde diese „Aufruf-Orgie“ nicht aufhören und die Funktion niemals einen sinnvollen Wert an den (oder die!) Aufrufer zurückgeben.

Der Schlüssel zum Verständnis sind hier die Zeilen 19-21:19 if (argument == 0) // Prüfe, ob das Ende der Rekursion erreicht ist20 // Rekursionsende („Bottom case“) erreicht, liefere Eins zurück21 return 1;

Hier wird anhand des Parameters argument irgendwann entschieden, nicht wieder den nächsten rekursiven Aufruf durchzuführen, sondern einfach zurückzuspringen. Somit wird die Rekursion genau dann unterbrochen, wenn der Parameter argument irgendwann den Wert Null inne hat. Das ist bei unseren Aufrufen der Funktion im­mer der Fall. Warum? Sehen Sie sich den rekursiven Aufruf in Zeile 24 einmal ge­nauer an:

24 return argument * fakultaet (argument – 1);Wie Sie sehen, wird der Wert des Parameters argument um Eins erniedrigt, bevor er an den nächsten Funktionsaufruf übergeben wird. Wenn wir mit einer Zahl n grö­ßer Null beginnen, wird nach genau n + 1 Aufrufen der Parameter den Wert Null ha­ben, und die Rekursion bricht ab. (Bei n = 0 wird die Funktion gleich beim ersten Aufruf in die ThenAnweisung der if-Anweisung verzweigen und gar keinen rekursi­ven Aufruf durchführen.)

Bei Rekursion müssen Sie besonders auf die Abbruchbedingung aufpassen! Wenn Sie beispielsweise unserer fakultaet-Funktion eine negative Zahl übergeben, gibt es ei­nen Programmabsturz zur Laufzeit, weil die Bedingung argument == 0 nie erfüllt wird. Die Grund für diesen Fehler ist die Tatsache, dass es sich um die Umsetzung einer

89

Sich selbst aufru­fen – geht das?

Abbruchbedin­gung ist notwen­dig

Gehen Sie sicher, dass die Rekursi­on auch abbricht!

Fallen bei der Verwendung von Rekursion

Page 98: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

bekannten mathematischen Funktion handelt, die für negative Zahlen nicht definiert ist. Was einen Mathematiker aber nicht stört, ist für uns an dieser Stelle fatal. Besser ist es, an dieser Stelle die Bedingung argument <= 0 zu verwenden, auch wenn es nicht der mathematischen Definition der Funktion entspricht.

Eine andere Möglichkeit ist es, einen vorzeichenlosen Datentyp (etwa unsigned int) für den Parameter zu verwenden; damit können negative Zahlen von vornherein verhin­dert werden.

Abbildung 18 versucht, die rekursiven Aufrufe bei der Auswertung des Ausdrucks fakultaet(3) zu veranschaulichen. Jeder Kasten ist ein Aufruf der Funktion. Die Kästen sind ineinander geschachtelt, weil die Funktion sich selbst aufruft.

Wozu Rekursion, wenn sie doch etwas gefährlich zu verwenden ist? Wir wollen an dieser Stelle nicht zu tief in die Diskussion „rekursiv vs. iterativ“ einsteigen, da diese Diskussion bereits seit über dreißig Jahren in der Informatik vehement geführt wird. Nur so viel dazu: Viele Algorithmen lassen sich mit Hilfe von Rekursion sehr leicht formulieren und programmieren, während die nicht-rekursive Formulierung komplex und unübersichtlich werden kann. (Die nicht-rekursiven Varianten werden iterativ

90

rekursiv vs. itera­tiv

Abbildung 18: Rekursion am Beispiel der Berechnung von 3! (3 Fakultät)

2. Aufruf: argument == 2→ Verzweigung in den else-Teil

1. Aufruf: argument == 3→ Verzweigung in den else-Teil

3. Aufruf: argument == 1→ Verzweigung in den else-Teil

4. Aufruf: argument == 0→ Verzweigung in den then-Teil→ Rückgabe von 1

→ Rückgabe von 1 × 0! = 1 × 1 = 1

→ Rückgabe von 2 × 1! = 2 × 1 = 2

→ Rückgabe von 3 × 2! = 3 × 2 = 6

Page 99: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

genannt, weil sie immer Schleifen zur Nachbildung der Rekursion verwenden.) Re­kursive Funktionen entsprechen auch viel stärker dem mathematischen Funktionsbe­griff, da es in der Mathematik keine Schleifen-Konstrukte für die Definition von Funktionen gibt. Iterative Algorithmen sind jedoch für die meisten Menschen leichter zu verstehen, auch wenn sie (wie oben gesagt) eher dazu neigen, lang und unüber­sichtlich zu werden.

Fairerweise muss man sagen, dass Sie auch bei nicht-rekursiven Algorithmen aufpas­sen müssen: wenn Sie nämlich Schleifen mit allgemeinen Bedingungen verwenden. Leicht kann man sich dabei eine sogenannte Endlos-Schleife einhandeln – also eine Schleife, die nicht abgebrochen wird und „ewig“ läuft (d. h. meistens bis die Res­sourcen des Rechners erschöpft sind). Sie können sich Schleifen nämlich ebenfalls rekursiv vorstellen: Nach dem Ende eines jeden Schleifendurchlaufs „ruft sich die Schleife selbst wieder auf“, solange die Abbruch-Bedingung nicht erfüllt ist.

Mein Ratschlag zur Frage „Rekursion oder Iteration“ ist eher pragmatischer Natur: Wenn Sie die offensichtliche Wahl zwischen einer iterativen und rekursiven Formu­lierung eines Algorithmus haben und die iterative nicht wesentlich komplexer oder schwieriger zu verstehen ist als die rekursive, dann wählen Sie diese. Ansonsten wählen Sie die rekursive Variante.

Merksatz 12: Überlege gut den Einsatz von rekursiven Funktionen!

Merksatz 13: Überlege gut den Einsatz von Schleifen!

3.7 Literaturempfehlungen[Strou00] ist ein umfangreiches Buch von Bjarne Stroustrup, dem Erfinder von C++. Neben einer ausführlichen Behandlung der Sprachkonzepte von C++ widmet sich das Buch auch der Standard-Bibliothek und Fragestellungen des Programm-Ent­wurfs. Das Buch deckt ziemlich alle Zielgruppen ab, vom C++-unerfahrenen Pro­grammierer bis hin zum Experten.

[Josu01] behandelt ebenfalls die Programmiersprache C++ relativ ausführlich, stellt aber die objektorientierten Konzepte in den Vordergrund. Das Buch ist bereits in die­sem Abschnitt (und nicht erst in Kapitel 4) aufgeführt, weil der nicht-objektorientier­te Teil von C++ gut dargestellt wird und auch C++-unerfahrene Programmierer durch viele Beispiele auf ihre Kosten kommen.

[Meye98] und [Meye99] versuchen nicht, Ihnen die Programmiersprache C++ in Ih­rer Ganzheit näher zu bringen, sondern wollen Ihnen helfen, häufig gemachte Fehler zu vermeiden und Sie für Probleme verschiedenster Art zu sensibilisieren. Kenntnis­se in C++ werden somit vorausgesetzt. Zum Teil werden die angeschnittenen The­men ziemlich technisch. Ein Stück weiter gehen [Sutt00] und [Sutt02], die fortge­schrittene Kenntnisse erfordern und Denkaufgaben samt Lösungen zu verschiedenen Themen wie Ausnahme-Sicherheit, objektorientierter Entwurf oder Optimierung ent­halten.

91

Page 100: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Grundlegende Konzepte Objektorientiertes C++ für Einsteiger

3.8 ÜbungenÜ4 (*2) Machen Sie sich mit den Wertebereichen der Datentypen in Ihrer speziel­

len C++-Implementierung vertraut! Was ist die kleinste, was die größte darstell­bare Ganzzahl, Fließkommazahl? Wie lang kann eine Zeichenkette maximal werden?

Ü5 (*1) Implementieren Sie die Funktion fakultaet aus Beispiel 3.6.5 iterativ, d. h. unter Zuhilfenahme von Schleifen!

Ü6 (*2) Implementieren Sie die fibonacci-Funktion sowohl rekursiv als auch iterativ! Die Funktion fibonacci(n) ist folgendermaßen definiert:

fibonacci(0) = 1

fibonacci(1) = 1

fibonacci(n) = fibonacci(n - 1) + fibonacci(n - 2) [für n > 1]

Ü7 (*1,5) Implementieren Sie den Euklidischen Algorithmus zur Berechnung des größten gemeinsamen Teilers zweier ganzer Zahlen größer Null! Die Funktion ggT(a, b) ist folgendermaßen definiert:

ggT(a, 0) = a

ggT(a, b) = ggT(b, a % b) [für b > 0]

Ü8 (*2) Implementieren Sie die ggT-Funktion iterativ!

Ü9 (*1) Welche Merksätze werden hier verletzt? Korrigieren Sie die Funktion dementsprechend!

1 using namespace std;2 string dezimalZuBinaer (int zahl)3 {4 string ergebnis = ""; // akkumuliert die umgewandelten Ziffern5 while (zahl > 0)6 {7 // berechne nächste Ziffer8 const int rest = zahl % 2; 9 // wandle in passendes Zeichen um

10 const char ziffer = '0' + rest;11 // packe die Ziffer vor das Ergebnis12 ergebnis = ziffer + ergebnis;13 // reduziere die umzuwandelnde Zahl14 zahl /= 2;15 }16 // führende Null, damit wir im Falle zahl == 0 keine leere Zeichenkette zurückgeben17 return "0" + ergebnis;18 }

Ü10 (*2) Schreiben Sie die (korrigierte) Funktion aus Übung 9 so um, dass die Basis als Parameter übergeben wird! Dabei dürfen Sie davon ausgehen, dass die Basis immer in dem Wertebereich zwischen 2 und 10 liegt! (Denken Sie daran, die Funktion geeignet umzubenennen!)

92

Page 101: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Grundlegende Konzepte

Ü11 (*1) Schreiben Sie eine Funktion max, die den Maximal-Wert zweier Operan­den zurückgibt! Implementieren Sie zwei Varianten: Eine mit if-Anweisung und eine mit dem Fallunterscheidungs-Operator!

Ü12 (*1,5) Schreiben Sie eine Funktion isRightTriangle, die ermittelt, ob drei Zahlen als Kantenlängen eines Dreiecks interpretiert ein rechtwinkliges Dreieck (Stichwort „Satz des Pythagoras“) ergeben!

Ü13 (*1,5) Schreiben Sie eine Funktion power zum Ermitteln der Potenz einer na­türlichen Zahl!29 Implementieren Sie diese sowohl iterativ als auch rekursiv!

Ü14 (*1) Schreiben Sie die Funktionen AND, OR und NOT, welche die Bedeutung der logischen Operatoren &&, || und ! abbilden, ohne diese jedoch zu nutzen! Implementieren Sie jeweils zwei Varianten: Eine mit if-Anweisung und eine mit dem Fallunterscheidungs-Operator!

Ü15 (*2) Schreiben Sie eine Funktion subString, die die Teilzeichenkette einer vorgegebenen Zeichenkette, angegeben über eine Start-Position (einschließlich) und eine End-Position (ausschließlich), liefert! Dabei sei der Index des ersten Zeichens Null.

Ü16 (*2,5) Schreiben Sie eine Funktion reverse, die eine string-Zeichenkette umdreht! Implementieren Sie diese sowohl iterativ als auch rekursiv!

Ü17 (*3) Schreiben Sie eine Funktion towers, die eine Anleitung für die Lösung des Türme-von-Hanoi-Problems für Türme beliebiger Höhe ausgibt!

Das Türme-von-Hanoi-Problem besteht aus einem Turm A aus n unterschied­lich großen Scheiben (die größte liegt zuunterst) und zwei „leeren“ Türmen B und C. Ziel ist es, alle Scheiben des Turms A zum Turm B zu bewegen. Dabei darf man jedoch niemals mehr als eine Scheibe gleichzeitig bewegen, und es darf niemals eine größere Scheibe auf eine kleinere gelegt werden. Auf einen „leeren“ Turm kann eine beliebig große Scheibe gelegt werden.

Ü18 (*5) Entwickeln und implementieren Sie für Übung 17 eine iterative Lösung!

29)x0=1

x y= x⋅x⋅...⋅xy−mal

93

Page 102: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,
Page 103: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

4 Die Welt der ObjekteIn diesem Kapitel lernen Sie die Grundlagen der objektorientierten Programmierung und erfahren, wie Sie die Konzepte in C++ umsetzen.

4.1 GrundlagenDieser Abschnitt lehrt Sie die Grundlagen der objektorientierten Programmierung. Sie erfahren, was ein Objekt (im Sinne des OO-Paradigmas) ist, was Klassen sind und wie Objekte und Klassen im Verhältnis zueinander stehen. Sie lernen, wie Klas­sen strukturiert sind und welche Elemente sie enthalten können. Schließlich erfahren Sie, wie Gemeinsamkeiten zwischen Objekten und Klassen in der objektorientierten Welt ausgedrückt werden.

In diesem Abschnitt werden die meisten allgemeinen Begriffe des objektorientierten Paradigmas erläutert. Einige etwas speziellere Begriffe werden jedoch in späteren Abschnitten erklärt, wenn sich dies anbietet.

Die erklärten Konzepte werden zusätzlich an einem einfachen Beispiel veranschaulicht. Dieses Beispiel dreht sich um Personen, die Video-Filme besitzen und sich diese gele­gentlich mit einem geeigneten Abspielgerät anschauen. Die Abschnitte, in denen die Konzepte an dem Beispiel erklärt werden, sind mit dem nebenstehenden Symbol ge­kennzeichnet. Hinweis: Jegliche in dem Beispiel verwendeten Produkte sind willkürlich gewählt; die Marken sind mit großer Wahrscheinlichkeit urheberrechtlich geschützt.

4.1.1 Objekte, Nachrichten, Operationen und MethodenWenn Sie noch nie objektorientiert programmiert haben, wenn das Thema für Sie völlig neu ist, dann sind erst einige Worte zum objektorientierten Paradigma nötig. Sie können sich die objektorientierte Welt vorstellen als ein System unterschiedli­cher, miteinander interagierender Objekte (Abbildung 19). Alles sind Objekte. Genau wie in der Realität ein Apfel, eine Schere oder ein Glas Objekte sind, die Sie anfas­

sen und mit denen Sie etwas anstellen können, sind Objekte in der objektorientierten Welt etwas Reales. Sie können Objekte erzeugen, mit ihnen arbeiten und sie nach ge­taner Arbeit auch wieder zerstören.

95

Überblick

System von Ob­jekten

Überblick über die Grundbegriffe

Abbildung 19: System interagierender Ob­jekte

Page 104: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

In unserem Beispiel sind Video-Filme Objekte, aber auch Video-Medien, die diese Fil­me enthalten; Video-Abspielgeräte, welche diese wiedergeben; und auch Personen, die sich diese Filme anschauen wollen. Einige Objekte samt möglicher Beziehungen sind in Abbildung 20 gezeigt.

In der objektorientierten Welt hat jedes Objekt gewisse Fähigkeiten. Diese Fähigkei­ten äußern sich dadurch, dass ein Objekt ein anderes bitten kann, etwas zu erledigen. Das eine Objekt schlüpft also in die Rolle eines Klienten, das andere in die Rolle ei­nes Dienstleisters. Dieses Ansprechen geschieht in Form von speziellen Nachrichten. Enthält eine Nachricht zusätzliche Informationen, die der Dienstleister für die Aus­führung des gewünschten Dienstes benötigt, spricht man von parametrisierten Nach­richten.

In unserem Beispiel hat Hans die VHS-Kassette 1 in das VHS-Abspielgerät JVC HR-S-5960 eingelegt und die Wiedergabe gestartet. Die Nachricht „Medium einlegen“ ist mit dem einzulegenden Medium parametrisiert. Dieser Vorgang wird in Abbildung 21 dar­gestellt.

„Versteht“ das Objekt diese Nachricht, so erfüllt es die ihm anvertraute Aufgabe, in­dem eine Methode ausgeführt wird. Ein Objekt versteht eine Nachricht nur, wenn eine entsprechende Operation vorhanden ist. Die Methode stellt den Programm-Code dar, der für diese Operation hinterlegt ist; dieses Hinterlegen von Code zu einer Ope­

96

Klienten, Dienst­leister, Nachrich­ten und Methoden

Abbildung 20: Video-Beispiel: Objekte und Beziehungen

Hans: Person

Schindlers Liste: Video-Film

Für eine Handvoll Dollar: Video-Film

Gummibärenbande: Video-Film

VHS-Kassette 1: VHS-Kassette

DVD 1: DVD

Philips DVP 520: DVD-Abspielgerät JVC HR-S 5960: VHS-Abspielgerät

Abbildung 21: Video-Beispiel: Klienten, Dienstleister und Nachrichten

Hansin der Rolle

eines Klienten

JVC HR-S-5960in der Rolle

eines Dienstleister

Dienst „Medium einziehen“Dienst „Wiedergabe starten“Dienst „Wiedergabe stoppen“Dienst „Medium auswerfen“

Nachricht „VHS-Kassette 1 einziehen“

Antwort „VHS-Kassette 1 eingezogen“

Nachricht „Wiedergabe starten“

Antwort „Wiedergabe gestartet“

Page 105: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

ration nennt man Implementierung der Operation. Ein Beispiel aus der realen Welt: Wenn ich (der Klient) Sie (den Dienstleister) auffordere, ein C++-Programm einzu­tippen, so müssen Sie meine Aufforderung (die Nachricht) erst einmal verstehen, d. h. den deutschen Satz in seine Bestandteile zerlegen und die assozierte Bedeutung erkennen (die Operation). Wenn Sie nach dem Verstehen der Aufforderung nach­kommen, reagieren Sie auf meine Aufforderung mit einer passenden Handlung (einer Methode), die Sie irgendwann gelernt (implementiert) haben.

In unserem Beispiel sind die Operationen des JVC HR-S-5960 allesamt implementiert, d. h. mit entsprechender Funktionalität ausgestattet. Dieser Sachverhalt wird durch Ab­bildung 22 veranschaulicht.

Versteht das Objekt diese Nachricht nicht, so liegt ein Programmfehler vor. Jedes Objekt versteht einen bestimmten Satz an Nachrichten. Wenn zwei verschiedene Ob­jekte aber dieselbe Nachricht verstehen, heißt das nicht unbedingt, dass sie daraufhin auch dasselbe tun, sprich dieselbe Methode ausführen. Das ist genauso wie in der realen Welt: Tragen Sie fünf Menschen auf, für einen bestimmten Betrag einzukau­fen, und sie bekommen mit hoher Wahrscheinlichkeit fünf unterschiedlich gefüllte Einkaufskörbe.

In unserem Beispiel versteht ein Abspiel-Gerät die Nachricht „Medium auswerfen“, das es dazu anleitet, ein Video-Medium herauszugeben; weitere sinnvolle Nachrichten sind „Wiedergabe starten“, „Wiedergabe stoppen“ sowie „Medium X einziehen“.

Zusammengefasst ergibt sich also:

• Eine Operation ist die Spezifikation eines Dienstes.

• Eine Methode ist die Realisierung eines Dienstes.

• Eine Nachricht ist die Anforderung eines Dienstes.

Wichtig ist noch herauszustellen, dass Objekte nur durch Nachrichten miteinander kommunizieren und somit nur ihre gegenseitigen Operationen kennen. Die Methode, die zu einer Operation gehört und sie quasi „mit Leben füllt“, ist nur dem jeweiligen Objekt bekannt. Diese Kapselung der Realisierung ist extrem wichtig für das Erstel­len flexibler objektorientierter Programme, weil hierdurch Abhängigkeiten von einer

97

Abbildung 22: Video-Beispiel: Operationen und Methoden

JVC HR-S-5960

Dienst „Medium einziehen“Dienst „Wiedergabe starten“Dienst „Wiedergabe stoppen“Dienst „Medium auswerfen“

... starte Motoren ...

... aktiviere Video-Kopf ...

... deaktiviere Video-Kopf ...

... starte Motoren (rückwärts) ...

Operationen, Me­thoden und Nach­richten im Ver­gleich

Implementierung ist versteckt

Page 106: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

ganz bestimmten Realisierungsstrategie vermieden werden. Die Implementierung ei­ner Operation (z. B. einer Sortier-Operation) kann folglich ausgetauscht werden, ohne dass sich für den Rest des Programms etwas ändert.30 Somit ist die Trennung von Operation und Methode ein wichtiges Standbein der Flexibilität und Wartbarkeit objektorientierter Software. Ein anderes Standbein ist Abstraktion auf Objektebene, die sie weiter unten kennen lernen werden.

4.1.2 Assoziationen und AggregationenDie objektorientierte Welt, so wie sie bisher geschildert worden ist, wäre aber ziem­lich langweilig, wenn es nicht Beziehungen zwischen den Objekten gäbe. In der rea­len Welt treten Beziehungen zwischen Objekten zuhauf zutage: Das Auto hat vier Räder (= HAT-EIN-Beziehung), der Chef kennt das Restaurant (= KENNT-EIN-Be­ziehung), der Schrank enthält die Akte (= HAT-EIN-Beziehung). Alle Beziehungen zwischen Objekten werden in der objektorientierten Welt Assoziationen genannt. Für besonders ausgezeichnete Assoziationen existieren gesonderte Ausdrücke: so wird etwa eine Ganzes-Teile-Beziehung Aggregation genannt (ein Rad IST EIN TEIL ei­nes Autos/ein Auto HAT EIN Rad). Es gibt noch andere Arten von Beziehungen (etwa Abhängigkeit), die Sie später kennen lernen werden.

In unserem Beispiel existiert eine Beziehung zwischen dem VHS-Abspielgerät JVC HR-S-5960 und der eingelegten VHS-Kassette 1 (Abbildung 20). Die eingelegte Kas­sette ist dem Abspielgerät bekannt. Sobald sie wieder entnommen wird, „vergisst“ das Abspielgerät die Kassette, die Beziehung zwischen den beiden Objekten verschwindet. Eine Aggregation wäre (die nicht gezeigte) Beziehung zwischen dem VHS-Abspielgerät JVC HR-S-5960 und seinem Video-Kopf, da letzterer ein Teil des Geräts ist.

Assoziationen spielen in der objektorientierten Programmierung eine große Rolle, weil sie die einzige Möglichkeit darstellen, Aufgaben an mehrere Objekte zu vertei­len. Schließlich können Sie nur dann Arbeit delegieren, wenn Sie wissen, wer diese Arbeit übernehmen kann. Da die konsequente Anwendung der objektorientierten Denkweise in der Regel zu Systemen mit vielen kleinen Objekten führt, die eine ganz bestimmte spezielle Aufgabe übernehmen, wird die Funktionalität des Gesamt-Programms durch die richtige „Verschaltung“ der Objekte untereinander erreicht, so ähnlich wie die Neuronen im menschlichen Gehirn nur durch die korrekte Verschal­tung untereinander unsere geistigen Fähigkeiten ermöglichen.

In unserem Beispiel kann das VHS-Abspielgerät JVC HR-S-5960 auf die eingelegte Kassette zugreifen und das Magnetband auslesen, um an die Video-Daten heranzukom­men. Dies kann es jedoch nur, solange die Kassette eingelegt ist, oder mit der obigen Terminologie formuliert: solange die Beziehung zwischen Abspielgerät und Kassette existiert.

4.1.3 Gemeinsamkeiten, Schnittstellen und PolymorphieDoch sind nicht alle Objekte völlig verschieden. Ähnlich wie in der Realität, in der ein Geschäft mehrere Kaffeemaschinen derselben Sorte verkauft, die sich in Hinblick auf Funktionalität nicht voneinander unterscheiden (sollen), kann es in der objektori­entierten Welt Objekte geben, die einander ähnlich sind. Diese Ähnlichkeit kann sich

30) bis auf nicht-funktionale Eigenschaften, etwa Performance

98

Beziehungen zwi­schen Objekten

Ähnlichkeiten zwischen Objek­ten

Arbeitsverteilung

Page 107: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

auf zwei unterschiedliche Aspekte beziehen: das Verstehen von Nachrichten und das Umsetzen dieser.

Wenn zwei Objekte dieselben Nachrichten verstehen, besitzen sie (zumindest teil­weise) dieselben Operationen. Dieser Satz an gemeinsamen Operationen bildet eine gemeinsame Schnittstelle oder einen gemeinsamen Typ. Diese Schnittstelle ist dann eine Möglichkeit für Klienten, beide Objekte gleichartig zu behandeln. Wenn ein Klient nur über diese Schnittstelle auf beide Objekte zugreift, verschwinden für ihn alle anderen, möglicherweise vorhandenen Unterschiede zwischen diesen Objekten. Die Schnittstelle ist also eine Abstraktion der Realität. Die Eigenschaft, dass ein Kli­ent unterschiedliche Objekte unter einer gemeinsamen Schnittstelle ansprechen kann, bezeichnet man als Polymorphie (Vielgestaltigkeit).

In unserem Beispiel verstehen das VHS-Abspielgerät JVC HR-S-5960 und das DVD-Abspielgerät Philips DVP 520 denselben Satz an Nachrichten: „Medium X einziehen“, „Medium auswerfen“, „Wiedergabe starten“ und „Wiedergabe stoppen“. Diese Opera­tionen bilden also eine gemeinsame Schnittstelle, die wir Video-Bedienung nennen wol­len, weil sie den Aspekt der Bedienung dieser beiden Geräte zusammenfasst (Abbildung23). Eine Person (etwa Hans) muss bei der Bedienung eines VHS- oder eines DVD-Ab­spielgeräts keine Unterschiede machen; sie greift also über die Schnittstelle Video-Be­dienung polymorph auf beide Geräte zu.

Im täglichen Leben abstrahieren Sie andauernd. Beispielsweise ist es Ihnen egal, ob Sie einen Kugelschreiber oder einen Bleistift erwischen, wenn Sie sich nur kurz et­was auf einem Zettel notieren wollen. Die für Sie in diesem Augenblick interessante Schnittstelle, die Schreibfähigkeit des Stifts, wird von beiden in Frage kommenden Utensilien „verstanden“. Da Sie nur an dieser Schnittstelle interessiert sind, treten alle anderen Unterschiede zwischen Kugelschreiber und Bleistift zurück.

Sie sehen an diesem Beispiel auch, dass die Sichtweise auf zwei Objekte zu verschie­denen Zeiten unterschiedlich sein kann. Ein andermal möchten Sie vielleicht ein wichtiges Dokument unterschreiben und haben wieder den Bleistift und den Kugel­

99

Abstraktion und gleiche Schnitt­stellen/Typen; Polymorphie

Abbildung 23: Video-Beispiel: Schnittstellen

VHS-Abspielgerät

Dienst „Medium einziehen“Dienst „Wiedergabe starten“Dienst „Wiedergabe stoppen“Dienst „Medium auswerfen“

DVD-Abspielgerät

Dienst „Medium einziehen“Dienst „Wiedergabe starten“Dienst „Wiedergabe stoppen“Dienst „Medium auswerfen“

<<interface>>Video-Bedienung

Dienst „Medium einziehen“Dienst „Wiedergabe starten“Dienst „Wiedergabe stoppen“Dienst „Medium auswerfen“

Person

<<realize>><<realize>>

Page 108: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

schreiber zur Auswahl. Nun sind Sie bei dem zu verwendenden Stift auch an der Fä­higkeit „dokumentenecht“ interessiert. Diese Eigenschaft kann als Schnittstelle in unserem Sinne aufgefasst werden: Stellen Sie sich vor, Sie schicken dem Objekt (Kugelschreiber oder Bleistift) die Frage „Bist du dokumentenecht?“ als Nachricht. Nur der Kugelschreiber wird mit „ja“ antworten, somit unterstützt in diesem Fall nur der Kugelschreiber die gewünschte Schnittstelle.

Wenn man von der Schnittstelle eines Objekts spricht, ohne konkret eine bestimmte zu benennen, so wird die vollständige Schnittstelle des Objekts gemeint, also alle Nachrichten, die das Objekt versteht.

In unserem Beispiel ist die Schnittstelle Video-Bedienung des JVC HR-S-5960 auch gleichzeitig dessen vollständige Schnittstelle, weil keine weiteren Dienste angeboten werden.

Die zweite Art der Ähnlichkeit geht noch weiter. Zwei Objekte können nämlich eine Nachricht nicht nur beide verstehen, sondern sich daraufhin gleich verhalten. Bei die­ser Art der Ähnlichkeit sind also nicht nur die beiden entsprechenden Schnittstellen gleich, sondern auch das zugehörige Verhalten, ihre Implementierung. Diese Form der Ähnlichkeit kann so weit gehen, dass sich zwei Objekte in allen Aspekten genau gleich verhalten.

Hätte Hans in unserem Beispiel zwei JVC HR-S-5960-Geräte, stellten beide dieselben Operationen zur Verfügung (Schnittstellen-Gleichheit) und verhielten sich exakt gleich (Gleichheit der Implementierung).

Eine Schnittstelle sagt nur etwas darüber aus, was ein Objekt an Dienstleistungen er­bringen kann. Das Wie wird dabei nicht spezifiziert. Objekte sind also weitestgehend unabhängig von der konkreten Umsetzung eines Dienstes. Dies macht objektorien­tierte Systeme so flexibel: Sie können die Implementierung einer Operation (eine Methode) verändern, ohne dass Sie irgendwelche anderen Teile des Systems anpas­sen müssen, da alle anderen Objekte die unveränderte Schnittstelle benutzen. Natür­lich funktioniert das nur, wenn Sie die Schnittstelle nicht verändern und auch die Be­deutung der Operation nicht „umbiegen“. Eine Operation „Wiedergabe starten“ sollte auch immer eine Wiedergabe starten und nicht beispielsweise Kaffee kochen.

Der aufmerksame Leser wird feststellen, dass Polymorphie nur eine konsequente Fortsetzung der Trennung von Schnittstelle und Implementierung ist, die wir bei Operationen und Methoden bereits kennen gelernt haben. Auch Operationen sind in diesem Sinne polymorph, weil der Klient nie wissen kann, wie die Methode hinter ei­ner Operation aussieht. Der Begriff „Polymorphie“ bezieht sich in der objektorien­tierten Welt üblicherweise jedoch auf die Austauschbarkeit von Objekten, also auf die Gemeinsamkeiten der Schnittstellen von Objekten und nicht von einzelnen Me­thoden. Das Konzept ist allerdings dasselbe. Somit ist Polymorphie, wie sie hier er­läutert ist, ein weiteres Standbein der Flexibilität und Wartbarkeit objektorientierter Software.

Operationen werden zur Verdeutlichung der Tatsache, dass sie über die Implementie­rung nicht aussagen, abstrakte Operationen genannt.

100

gleiche Imple­mentierung

vollständige Schnittstelle

Schnittstellen spezifizieren das „Was“

konsequente Trennung von Schnittstelle und Implementierung

Page 109: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

In unserem Beispiel spezifiziert die Schnittstelle Video-Bedienung nicht, wie die Diens­te erbracht werden. Sie werden von den beiden Video-Abspielgeräten JVC HR-S-5960 und Philips DVP 520 ganz unterschiedlich implementiert. Beispielsweise aktiviert erste­rer beim Start der Wiedergabe einen Video-Kopf, um Informationen von einem Magnet­band einzulesen, während letzterer einen Laser in Anspruch nimmt, um Vertiefungen in der DVD zu erkennen. Von der Bedeutung her sind aber beide gleich: Sie starten die Wiedergabe des eingelegten Video-Mediums.

Im vorletzten Abschnitt haben Sie gelernt, dass Objekte voneinander nur die jeweiligen Schnittstellen kennen. Deshalb ist es in der Software-Entwicklung sehr wichtig, diese Schnittstellen gut zu dokumentieren (z. B. über entsprechende Kommentare). Wenn die Schnittstellen schlecht dokumentiert sind, wird keiner die Dienstleistungen eines Ob­jekts in Anspruch nehmen, das über diese Schnittstelle angesprochen werden kann – da keiner voraussehen kann, was wirklich passiert! Deshalb ist eine gute Dokumentation hier das A und O: Denken Sie immer an Merksatz 2!

4.1.4 AttributeEin Attribut ist im Prinzip die Definition einer Variable, die an ein Objekt gebunden ist. Zur Laufzeit hat jedes Objekt zu jedem seiner Attribute einen passenden Wert. Die Gesamtheit aller Werte zu einem Objekt bildet den Zustand des Objekts. Die Werte, die den Zustand eines Objekts bilden, sind nach außen hin nicht sichtbar; Kli­enten „sehen“ den Zustand nur indirekt, indem sie Nachrichten an das Objekt schi­cken, das entsprechend antwortet. Man spricht in dem Zusammenhang auch von Kapselung und Information Hiding, weil das Objekt seinen internen Zustand vor sei­nen Klienten wie in einer Kapsel versteckt.

In unserem Beispiel hat ein Video-Abspielgerät die Attribute „Marke“ und „Kaufpreis“ (Abbildung 24). Da es unseren Abspielgeräten an geeigneten Operationen zum Ausle­sen dieser Informationen fehlt, sind diese Attribute für alle potentiellen Klienten un­sichtbar (und somit nutzlos!)

Ein Wert zu einem Attribut ist ansonsten ein gewöhnliches Objekt mit Operationen, Methoden, eigenen Attributen etc. Man könnte die Beziehung zwischen Objekt und Wert bzw. zwischen Klasse (s. u.) und Attribut auch als besondere Beziehung auffas­sen. Darauf geht Abschnitt 4.4.4 genauer ein.

4.1.5 KlassenSie haben gelernt, dass eine Schnittstelle einem Satz von Operationen entspricht. Eine Klasse ist auch eine Schnittstelle, allerdings greift der Begriff Klasse noch et­was weiter. Eine Klasse kann zusätzlich zu Operationen

101

Klassen spezifi­zieren das „Wie“

Schnittstellen müssen dokumen­tiert werden

Attribute sind Da­ten von Objekten

Abbildung 24: Video-Beispiel: Attribute

JVC HR-S-5960

Dienst „Medium einziehen“Dienst „Wiedergabe starten“Dienst „Wiedergabe stoppen“Dienst „Medium auswerfen“

Attribut „Marke“ = „JVC“Attribut „Kaufpreis“ = „100 EUR“

Philips DVP 520

Dienst „Medium einziehen“Dienst „Wiedergabe starten“Dienst „Wiedergabe stoppen“Dienst „Medium auswerfen“

Attribut „Marke“ = „Philips“Attribut „Kaufpreis“ = „60 EUR“

Page 110: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

• Methoden und

• Attribute

besitzen. Während eine Schnittstelle immer nur aussagt, was ein Objekt tun kann bzw. was es an Nachrichten versteht (Operationen), sagt eine Klasse nun etwas dar­über aus, wie das Objekt etwas tut (Methoden).

Somit gehören Objekte immer einer (konkreten, s. u.) Klasse an, weil ein Objekt im­mer auf alle Nachrichten reagieren muss, die es versteht, sprich zu jeder angebotenen Operation eine Methode parat haben muss. Man spricht davon, dass jedes Objekt ein Exemplar bzw. eine Instanz seiner Klasse ist.

Dementsprechend wird der Begriff Klasse auch verständlich. Alle Objekte derselben Klasse verstehen dieselben Nachrichten (da sie dieselbe Schnittstelle besitzen) und implementieren dasselbe Verhalten (da sie dieselben Methoden enthalten). Somit wird mit einer Klasse eine bestimmte Gruppe oder Klasse von Objekten beschrieben. Jedes dieser Objekte ist allen anderen Objekten derselben Klasse in Form (Schnitt­stelle) und Verhalten (Implementierung) gleich.

In unserem Beispiel haben wir bereits folgende konkrete Klassen kennen gelernt: „Per­son“, „VHS-Abspielgerät“, „DVD-Abspielgerät“, „VHS-Kassette“ und „DVD“.

Jede Schnittstelle ist auch eine Klasse, weil eine Klasse keine Methoden oder Attri­bute enthalten muss. Klassen, die nicht für alle Operationen eine Methode anbieten, werden abstrakt genannt. Zu einer abstrakten Klassen kann es keine Objekte geben. Im Gegenzug wird eine Klasse, die für jede Operation eine entsprechende Methode definiert, eine konkrete Klasse genannt.

In unserem Beispiel ist die Schnittstelle „Video-Bedienung“ eine solche abstrakte Klas­se.

Zwischen einer Schnittstelle und einer abstrakten Klasse völlig ohne Methoden und At­tribute gibt es faktisch keinen Unterschied. Beispielsweise werden beide Konzepte in C++ durch dasselbe Sprachmittel beschrieben. Allerdings gibt es Programmiersprachen wie Java und Notationen wie UML, die zwischen Schnittstellen und abstrakten Klassen einen Unterschied machen. Inbesondere wird die Konkretisierung von Klassen Verer­bung und die von Schnittstellen Spezialisierung genannt (4.1.6).

Attribute, die innerhalb einer Klasse definiert sind, haben eine „Ganz oder gar nicht“-Eigenschaft: Jedes Objekt hat zu jedem Attribut seiner Klasse einen Wert – und zwar vom Zeitpunkt seiner Erzeugung bis hin zu seiner Zerstörung. Bei der Er­zeugung eines Objekts werden die Attribute mit passenden Werten belegt. Im Ver­lauf des Programms können sich diese Werte verändern, nämlich dann, wenn das Objekt seinen Zustand ändert. Dies geschieht in der Regel als Reaktion auf eine An­forderung eines Klienten.

In unserem Beispiel sind die Attribute der Abspielgeräte konstant, weil sich der Kauf­preis und die Marke eines Geräts nach der Herstellung nicht mehr ändert. Hätten wir je­doch ein Attribut „Wiedergabe aktiv“, änderten die konkreten Abspielgeräte bei jeder Inanspruchnahme der Operationen „Wiedergabe starten“ und „Wiedergabe beenden“ den Wert des Attributs und somit ihren Zustand.

102

abstrakte und konkrete Klassen

Werte sind Bele­gungen von Attri­buten

Objekten sind Ex­emplare von Klassen

Page 111: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

Ähnlich verhält es sich mit Operationen und Methoden: Jedes Objekt hat zu jeder Operation in seiner vollständigen Schnittstelle eine passende Methode. Allerdings ist diese Beziehung zwischen Operation und Methode unveränderlich: Sie können zur Laufzeit nicht bei einer Operation die Methode durch eine andere austauschen, wäh­rend sie den Wert zu einem Attribut durchaus ändern können. Die Operationen, die ein Objekt versteht, und die Methoden, die diese implementieren, bleiben also in der gesamten Lebenszeit des Objekts konstant; sie werden durch die Klasse des Objekts beim Erzeugen des Objekts eindeutig festgelegt.

Der letzte Absatz trifft nicht immer zu. So gibt es Programmiersprachen (z. B. Small­talk), bei denen diese Zuordnung zwischen Objekt und unterstützten Schnittstellen bzw. verstandenen Nachrichten nicht so einfach durchzuführen ist, weil es keine Repräsenta­tion für Schnittstellen in der Sprache gibt und weil ein Objekt auch auf Nachrichten rea­gieren kann, die es eigentlich nicht versteht.31

4.1.6 Erweiterung, Spezialisierung und VererbungGenauso wie Beziehungen zwischen Objekten existieren, gibt es auch Beziehungen zwischen Schnittstellen und zwischen Klassen. Diese werden im Folgenden kurz vor­gestellt.

Schnittstellen können durch weitere Operationen „angereichert“ werden. Diesen Vor­gang nennt man Erweiterung der Schnittstelle. Die ursprüngliche, zu erweiternde Schnittstelle (oder den ursprünglichen Typ) nennt man auch den Supertyp, Obertyp oder Basistyp, die resultierte erweiterte Schnittstelle nennt man den Subtyp, Untertyp oder abgeleiteten Typ. (Alle diese Begriffe existieren auch mit -klasse am Ende, also etwa Oberklasse oder Unterklasse.)

In unserem Beispiel haben wir keine derartige Erweiterung, aber Sie könnten sich vor­stellen, dass wir eine Schnittstelle „Erweiterte Video-Bedienung“ definieren. Diese Schnittstelle könnte die Schnittstelle „Video-Bedienung“ um die Operationen „Vorspu­len“, „Zurückspulen“ und „Pausieren“ erweitern (Abbildung 25).

Eine weitere Beziehung zwischen Schnittstellen und/oder Klassen ist die Spezialisie­rung. Dabei wird eine Schnittstelle oder Klasse durch Methoden (Verhalten) oder Daten (Attribute) angereichert. Bei der Zuordnung einer Methode zu einer Operation sprechen wir von der Implementierung oder auch Definition der Operation. Das Er­gebnis einer Spezialisierung ist auf jeden Fall eine (abstrakte oder konkrete) Klasse. Diese Beziehung wird Spezialisierung genannt, weil die resultierende Klasse durch

31) Das hört sich vielleicht paradox an, ist aber wahr. Sie können in Smalltalk für ein Objekt eine Me­thode namens doesNotUnderstand definieren, die alle Nachrichten empfängt, die das Objekt nicht versteht, und dort geeignet reagieren.

103

Beziehungen zwi­schen Typen

Spezialisierung von Schnittstellen und Klassen

Methoden sind Belegungen von Operationen

Abbildung 25: Video-Beispiel: Erweiterung einer Schnittstelle

<<interface>>Video-Bedienung

Dienst „Medium einziehen“Dienst „Wiedergabe starten“Dienst „Wiedergabe stoppen“Dienst „Medium auswerfen“

<<interface>>Erweiterte Video-Bedienung

Dienst „Vorspulen“Dienst „Zurückspulen“Dienst „Pausieren“

Erweiterung von Schnittstellen

Page 112: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

das Mehr an Methoden und/oder Attributen konkreter und damit spezieller ist als die ursprüngliche.

In unserem Beispiel ist die Beziehung zwischen einem VHS-Abspielgerät und einem Video-Abspielgerät eine Spezialisierung, denn jedes VHS-Abspielgerät implementiert die Operationen eines Video-Abspielgeräts. Ferner muss jedes Video-Abspielgerät die Schnittstelle „Video-Bedienung“ realisieren; dies drücken wir ebenfalls über eine Spezi­alisierung aus (Abbildung 26).

Eine Variante der Spezialisierung ist die Redefinition. Dabei werden keine Operatio­nen, Methoden oder Attribute hinzugefügt. Vielmehr wird eine im Basistyp existie­rende Methode durch eine andere Methode ersetzt oder redefiniert. Diese Redefiniti­on sollte natürlich so erfolgen, dass Klienten des Basistyps auch weiterhin funktionieren, d. h. das Verhalten der Methode im abgeleiteten Typ sollte das Ver­halten der Methode im Basistyp umfassen.

Schließlich gibt es noch den Begriff der Vererbung, sozusagen die eierlegende Woll­milchsau. Die Vererbung steht für alle drei oben genannten Beziehungen zwischen zwei Schnittstellen bzw. Klassen. Wenn eine Klasse also von einer anderen erbt, kann das bedeuten, dass sie:

(1) die Schnittstelle der Basis-Klasse erweitert (Erweiterung) und/oder

(2) Operationen definiert oder Attribute hinzufügt (Spezialisierung) und/oder

(3) Methoden redefiniert (Redefinition).

Insbesondere der letzte Punkt in der obigen Aufzählung ist typisch beim Einsatz von Vererbung. In C++ ist auch der Begriff ableiten gebräuchlich.

Alle diese Beziehungen sind allesamt sogenannte „IST-EIN-“, „FUNKTIONIERT-WIE-“ oder „KANN-BENUTZT-WERDEN-WIE-“Beziehungen. Das bedeutet, dass auf der einen Seite der Beziehung eine Schnittstelle/Klasse steht, die an Stelle der Schnittstelle/Klasse auf der anderen Seite der Beziehung verwendet werden kann.

104

Redefinition von Methoden

Vererbung – alles zusammen

Ersetzbarkeit und LSP

Abbildung 26: Video-Beispiel: Spezialisierung von Schnittstellen und Klassen

<<interface>>Video-Bedienung

Dienst „Medium einziehen“Dienst „Wiedergabe starten“Dienst „Wiedergabe stoppen“Dienst „Medium auswerfen“

<<realize>>

Video-Abspielgerät

Dienst „Medium einziehen“Dienst „Wiedergabe starten“Dienst „Wiedergabe stoppen“Dienst „Medium auswerfen“

VHS-Abspielgerät

Dienst „Medium einziehen“Dienst „Wiedergabe starten“Dienst „Wiedergabe stoppen“Dienst „Medium auswerfen“

Page 113: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

In unserem Beispiel gilt: Ein Video-Abspielgerät KANN-BENUTZT-WERDEN-WIE eine Video-Bedienung (besseres Deutsch wäre in diesem Fall: KANN-BENUTZT-WERDEN-ÜBER). Ein VHS-Abspielgerät IST EIN Video-Abspielgerät.

IST-EIN-Beziehungen oder generell Beziehungen zwischen Schnittstellen bzw. Klassen sind starre Beziehungen – sie ändern sich nicht im Programm-Verlauf. Deswegen ist es sehr wichtig, dass die abgebildeten Beziehungen auch „stimmig“ sind. Beispielsweise IST ein Student NICHT eine Person, denn er wird nicht als Student geboren bzw. beer­digt. Vielmehr ist ein Student eine bestimmte Rolle einer Person, die sie im zeitlichen Verlauf bekommen und auch wieder ablegen kann. Solche „dynamischen“ Typ-Zuwei­sungen sind über Spezialisierung und Vererbung nicht direkt herstellbar. Sie spielen aber in flexiblen, objektorientierten Entwürfen eine große Rolle (!) und werden häufig in Entwurfsmustern (6) verwendet.

Diese Ersetzbarkeit, die bei Erweiterung, Spezialisierung und Vererbung vorliegt, wird in dem Liskov’schen Substitutionsprinzip (LSP)32 formalisiert, welches genau beschreibt, wann eine solche „IST-EIN-“Beziehung vorliegt, nämlich wenn man in einem objektorientierten Programm Objekte einer Klasse durch Objekte einer ande­ren Klasse ersetzen kann, ohne dass sich die Bedeutung des Programms ändert. Das Liskov’sche Substitutionsprinzip spielt auch bei der Redefinition von Methoden eine große Rolle; siehe hierzu Abschnitt 4.6.4.1.

Vererbungs-Beziehungen zwischen Klassen erstrecken sich manchmal über mehrere Ebenen. Beispiel: Ein PKW IST EIN Auto IST EIN Fahrzeug. Man spricht in solchen Fällen von Vererbungslinien. Wenn mehrere Vererbungslinien in einer gemeinsamen Wurzel zusammenlaufen und somit eine Art Baumstruktur entsteht, spricht man gar von ganzen Vererbungshierarchien.

4.2 Ein erstes objektorientiertes ProgrammFalls Ihnen nach dem letzten Abschnitt der Kopf raucht, nur nicht verzweifeln! Las­sen Sie uns gemeinsam ein erstes objektorientiertes Programm schreiben. Starten Sie also Ihren Lieblings-Editor und tippen Sie Folgendes ein:

1 /*** Beispiel oohello.cpp ***/2 #include <istream>3 #include <ostream>4 #include <iostream>5 #include <string>6 using namespace std;78 // diese Klasse begrüßt eine Person mit „Hallo“9 class HalloBegruessung10 {11 public :12 // begrüßt „person“ mit „Hallo“13 void begruesse (string person);14 };1516 void HalloBegruessung::begruesse (string person)17 {18 cout << "Hallo " << person << endl;19 }2021 int main ()

32) vgl. [Mart96]

105

Vererbungshier­archien

„hello“ einmal objektorientiert

Page 114: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

22 {23 cout << "Bitte geben Sie Ihren Namen ein: ";24 string name;25 cin >> name;2627 // erzeuge Objekt28 HalloBegruessung begruessung;29 // sende Nachricht „begruesse“ an das Objekt „begruessung“,30 // zusammen mit dem Argument „name“31 begruessung.begruesse (name);3233 return 0;34 }

Lassen Sie uns die neuen objektorientierten Elemente untersuchen:

• Zeilen 8-14: Hier definieren wir eine Klasse HalloBegruessung. Klassen sind Schablonen für Objekte und definieren, welche Daten diese Objekte enthal­ten und welche Nachrichten sie verstehen. In unserem Beispiel haben Objekte der Klasse HalloBegruessung keine Daten, verstehen aber die Nachricht begruesse. Dies wird durch die Definition der Operation begruesse in Zei­le 13 erledigt. Da die Operation einen Parameter person vom Typ string be­sitzt, müssen entsprechende Nachrichten an das Objekte immer ein Argument vom Typ string enthalten.

• Zeilen 16-19: Hier wird die Methode zu der Operation begruesse definiert. Die Methode definiert, was beim Erhalten der Nachricht begruesse tatsäch­lich getan wird. In unserem Fall wird einfach eine passende Begrüßung ausgege­ben.

• Zeile 28: Hier wird ein Objekt der Klasse Hallo erzeugt. Wie Sie sehen, ist das eine ganz normale Variablen-Definition, nur dass als Typ eben eine Klasse und nicht ein primitiver Datentyp (wie int) verwendet wird.

• Zeile 31: Hier wird an das Objekt begruessung mit Hilfe des Punkt-Opera­tors (.) die Nachricht begruesse verschickt. Das Verschicken einer Nachricht wird – bis auf den Unterschied mit dem Punkt-Operator – ansonsten genauso no­tiert wie der Aufruf einer Funktion. Insbesondere werden Daten, die zu einer Nachricht gehören, wie Argumente an die Methode übergeben.

Sie haben also an diesem Beispiel bereits vier grundlegende C++-Sprachmittel ken­nengelernt:

• Definieren einer Klasse samt Operationen

• Definieren von Methoden

• Erzeugen von Objekten

• Versenden von Nachrichten

Im Laufe des Skripts werden Sie weitere Sprachelemente kennen lernen und wann man diese geeignet einsetzt.

106

Definieren einer Klasse und ihrer Operationen

Definieren einer Methode

Erzeugen von Ob­jekten

Versenden von Nachrichten

Page 115: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

Vielleicht werden Sie einwenden, dass die Nutzung objektorientierter Techniken bei diesem speziellen Beispiel keine nennenswerten Vorteile bringen. Das ist richtig; ob­jektorientiertes Entwickeln kann erst bei größeren Projekten seine Vorteile ausspie­len. Zur Demonstration grundlegender objektorientierter Konzepte in C++ taugt es jedoch allemal. Und ein 500-Zeilen-Programm würde Sie ohnehin nur verschrecken...

4.3 UML (Unified Modeling Language)Zur Darstellung von Klassen, Objekten und Beziehungen hat sich die UML als Nota­tion durchgesetzt. Die UML ist eine Sprache zur Darstellung von Konstrukten aus dem Umfeld objektorientierter Technologien. Wir wollen in diesem Abschnitt die wichtigsten Diagramme vorstellen.

107

UML als Notation für OO-Entwick­lung

Abbildung 27: Das Video-Beispiel als UML-Klassendiagramm

Video-Medium

VHS-Kassette DVD

Video-Film

Video-Abspielgerät

VHS-Abspielgerät DVD-Abspielgerät

Person

<<interface>>Video-Bedienung<<realize>>

marke: string

Tonkopf Videokopf Motor

0..*0..*

start()stop()wirfAus():Video-Medium

start()stop()wirfAus(): DVDnimm(med: DVD)

start()stop()wirfAus(): VHS-Kassettenimm(med: VHS-Kassette)

titel: stringlängeInMinuten: int

beschreibbar: bool

längeInMinuten: int groesseInMB: int

◄ besitzt

inhalt

0..*

benutzt ►

111 1

Laser

1

0..1

0..1

enth

ält ►

enth

ält ►

Fernbedienung <<realize>>

1

ziel

name: string

◄ beinhaltet

Page 116: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

4.3.1 KlassendiagrammeIn einem UML-Klassendiagramm werden Klassen miteinander in Beziehung gesetzt. Abbildung 27 zeigt ein Klassendiagramm, das die Klassen und Beziehungen aus un­serem Video-Beispiel zusammenfassend darstellt. Es folgen die Erläuterungen zu dem Diagramm:

• Alle Klassen (inklusive Schnittstellen) werden in UML durch Kästen dargestellt. Ein solcher Kasten besteht aus drei Teilen: Im obersten wird der Name der Klas­se notiert, es folgen die Attribute und schließlich die Operationen bzw. Metho­den. Der Teil mit den Attributen bzw. Methoden kann entfallen, wenn die Klasse keine Attribute bzw. Methoden enthält.

• Abstrakte Klassen wie Video-Abspielgerät oder Video-Medium werden in der UML-Notation durch einen kursiv gesetzten Namen oder durch den Zusatz {ab­stract} gekennzeichnet.

• Schnittstellen werden wie Klassen dargestellt, aber zusätzlich mit dem Stereotyp <<interface>> markiert. Es dürfen keine Attribute oder Methoden (s. u.) vor­handen sein.

• Die Implementierung einer Schnittstelle wird in der UML-Notation durch einen gestrichelten Pfeil mit hohler Pfeilspitze und dem Stereotyp <<realize>> darge­stellt, wobei die speziellere Klasse auf die allgemeinere Schnittstelle zeigt.

• IST-EIN-Beziehung zwischen Klassen und generell alle Vererbungsbeziehun­gen, die nicht Spezialisierung von Schnittstellen sind, werden in der UML durch einen durchgezogenen Pfeil mit hohler Pfeilspitze dargestellt, wobei die speziel­lere Klasse auf die allgemeinere zeigt.

• Abstrakte Operationen werden kursiv geschrieben. Operationen werden in den dritten Teil eines Klassen-Kastens platziert; wenn keine Attribute vorhanden sind, können sie auch im zweiten Teil stehen.

• Attribute werden in den zweiten Teil eines Klassen-Kastens eingefügt.

• Methoden in implementierenden Klassen werden wie Operationen notiert, aller­dings diesmal nicht kursiv. Dadurch wird sichtbar, dass für diese Operationen in dieser Klasse Methoden existieren.

• Normale Beziehungen werden durch einen durchgezogenen Pfeil dargestellt, wo­bei der Pfeil vom Enthaltenden zum Enthaltenen, vom Kennenden zum Gekann­ten, vom Verwender zum Verwendeten u. s. w. zeigt. An der Pfeilspitze steht eine Multiplizität: 0..* bedeutet „beliebig viele“, 1 „genau ein“, 1..* „mindestens ein“, 3..4 „drei bis vier“ u. s. w. An den beiden Enden des Pfeils können Rollen-Bezeichnungen stehen; so nimmt der Video-Film beispielsweise in der Bezie­hung zwischen Medium und Film die Rolle des Inhalts ein. Schließlich kann der Assoziations-Pfeil genauer durch entsprechende Beschriftungen erläutert wer­den; im Beispiel wird die erwähnte Beziehung als „beinhaltet ►“ tituliert, so dass klar ist, dass ein Video-Medium Video-Filme beinhaltet (und nicht etwa be­sitzt, benutzt o. ä.)

108

Beziehungen zwi­schen Klassen

Klassen

(abstrakte) Ope­rationen

Attribute

Methoden

Assoziationen

abstrakte Klassen

reine Schnittstel­len

Spezialisierung von Schnittstellen

Spezialisierung von Klassen; Ver­erbung

Page 117: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

• In der UML werden Aggregationen als Linien mit einer hohle Raute bei jener Klasse markiert, das in der Ganzes-Teile-Beziehung das Ganze repräsentiert. In unserem Beispiel besteht ein VHS-Abspielgerät aus Tonköpfen, Video-Köpfen und Motoren.33

4.3.2 AktivitätsdiagrammeWährend Klassendiagramme die Struktur eines objektorientierten Programms be­schreiben und darstellen, wie Objekte der entsprechenden Klassen zur Laufzeit des Programms miteinander in Beziehung stehen, ist der Zweck von Aktivitätsdiagram­men die Darstellung von Algorithmen. Dementsprechend sind sie ähnlich aufgebaut wie Programmablaufpläne oder Petri-Netze; wenn Sie mit den Notationen vertraut sind, sollten Sie keinerlei Probleme haben, die UML-Notation zu verstehen.

Abbildung 28 stellt das Beispiel zur do-Schleife aus Abschnitt 3.5.4.2 dar.

An den Diagramm können Sie grundlegende Elemente gut erkennen. Aktionen oder Aktivitäten werden durch „Beinahe-Rechtecke“ dargestellt. Jede Aktivität steht für eine Aktion des Programms, oder für mehrere Aktionen, die zu einem logischen

Ganzen verbunden sind und entweder alle zusammen oder gar nicht ausgeführt wer­den. Jede Aktivität enthält in ihrem Kasten eine kurze Zusammenfassung ihrer Wir­kung, etwa „Name einlesen“.

33) sowie aus vielen anderen Teilen, die aus Gründen der Übersichtlichkeit und der Unkenntnis des Autors bewusst weggelassen wurden

109

Aggregationen

Beziehungen zwi­schen Aktivitäten

Aktivitäten

Abbildung 28: Aktivitätsdiagramm zum do-Beispiel (3.5.4.2)

Name initialisieren

Anfrage ausgeben

Name einlesen

Name zurückgeben

[Name leer][Name nicht leer]

Page 118: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

Übergänge zwischen den einzelnen Aktivitäten werden Transitionen genannt und durch Pfeile dargestellt. Eine einfache Transition zwischen zwei Aktivitäten bedeu­tet, dass beim Programmablauf die Kontrolle von der ersten Aktivität zur zweiten übergeht, und zwar ohne Ausnahme.

Rauten kennzeichnen Verzweigungen. Eine Verzweigung wird immer dann verwen­det, wenn es mehrere Möglichkeiten gibt, nach der Ausführung einer Aktivität wei­terzumachen. An einer Verzweigungsstelle gibt es also mindestens zwei Pfeile zu un­terschiedlichen Aktivitäten. An den Pfeilen stehen innerhalb von eckigen Klammern Bedingungen, welche die Umstände spezifizieren, unter denen eine Transition ausge­wählt wird. Im Beispiel hängt es vom eingelesenen Namen ab (leer oder nicht leer), ob nach dem Einlesen die Aktivität „Name zurückgeben“ oder „Anfrage ausgeben“ ausgeführt wird.

Nicht im Diagramm dargestellt, aber ebenfalls möglich sind Zusammenführungen von Verzweigungen an einer Raute. Dabei münden mehrere Kontrollfluss-Stränge an einer solchen Zusammenführung, und ein einzelner Pfeil führt zu der Aktivität, die der Zusammenführung folgt. Bei einer solchen Zusammenführung stehen natürlich keine Bedingungen an den Pfeilen. So eine Zusammenführung ist beispielsweise not­wendig, wenn einer if-Anweisung weitere Anweisungen folgen.

Schleifen werden einfach dargestellt, indem ein Pfeil zu einer Aktivität „weiter oben“ im Diagramm geführt wird. Schleifen werden immer mit einer passenden Ver­zweigung gekoppelt, die dafür sorgt, dass die Schleife auch wieder verlassen werden kann.

4.4 Konkrete Datentypen: Daten und Methoden kapselnIn diesem Abschnitt werden Sie mit der objektorientierten Programmierung in C++ am Beispiel konkreter Klassen vertraut gemacht. Sie lernen in diesem Abschnitt, wie Sie mit dem objektorientierten Paradigma im Hinterkopf an ein Problem herantreten und es lösen. Nach dem Lesen dieses Abschnitts werden Sie auch wissen, wie Sie konkrete Klassen, Methoden und Objekte in C++ definieren und verwenden können.

4.4.1 ProblemstellungEs ist soweit: Sie lösen Ihr erstes Programmierproblem auf die objektorientierte Art und Weise. Die Aufgabe lautet folgendermaßen:

Implementieren Sie einen Queue-Container, der nach dem FIFO-Prinzip arbeitet! Die Queue muss:• neue Objekte aufnehmen können,• bestehende Objekte entfernen können,• die Information zur Verfügung stehen, ob sie Elemente

enthält oder nicht.

110

Zusammenfüh­rungen

Transitionen

Verzweigungen

Schleifen

objektorientierte Vorgehensweise

die zu lösende Aufgabe

Page 119: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

4.4.2 Analyse-Phase

4.4.2.1 FachlexikonIn der Analyse-Phase versuchen wir zuerst, das Problem zu verstehen. Zuerst müssen wir unbekannte Begriffe klären:

• Queue: Englischer Begriff für →Schlange• Schlange: ein →Behälter für Objekte, der nach einem ganz bestimmten Prinzip,

dem →FIFO-Prinzip, arbeitet.

• Container: englischer Begriff für →Behälter• Behälter: ein Objekt, das →Elemente enthält und Operationen zum Verwalten

dieser Elemente anbietet, etwa Hinzufügen, Entfernen oder Suchen.

• FIFO: FIFO steht für „First in, first out“ und bedeutet, dass das zuerst in einen →Behälter hineingesteckte →Element auch zuerst wieder herauskommt. Gegen­satz: →LIFO. Siehe auch →Schlange

• LIFO: LIFO steht für „Last in, first out“ und bedeutet, dass das zuletzt in einen →Behälter hineingesteckte →Element zuerst wieder herauskommt. Gegensatz: →FIFO. Siehe auch →Stapel.

• Stapel: ein →Behälter, der nach dem →LIFO-Prinzip arbeitet.

• Element: ein Objekt, das sich in einem →Behälter befindet.

Wie Sie sehen, sind beim Analysieren einige neue Informationen hinzugekommen, die aus dem Umfeld der Aufgabenstellung stammen. Das ist typisch für die Analyse­phase: Anfangs kann man normalerweise gar nicht absehen, welche Informationen später noch von Bedeutung sind und welche nicht.

Diese erarbeiteten Begriffsdefinitionen werden in einem sogenannten Fachlexikon zusammengefasst. Dieses Fachlexikon enthält Ihr Wissen zur Aufgabenstellung und ist nutzlos, wenn die enthaltenen Informationen falsch sind. Ein gutes Fachlexikon enthält nicht nur korrekte Definitionen der Begriffe, sondern auch Informationen über die Beziehungen zwischen den Begriffen (Querverweise).

Denken Sie nicht, dass Ihr Fachlexikon bereits am Anfang der Software-Entwicklung fertig ist und keinerlei Veränderungen mehr bedarf! Oft stellt sich während der nächsten Phasen heraus, dass einige Begriffe noch fehlen, zu ungenau formuliert sind oder für die weitere Software-Entwicklung nicht von Bedeutung sind. Machen Sie sich also auf die kontinuierliche Pflege des Fachlexikons gefasst.

4.4.2.2 Fachklassen-DiagrammWährend der Beschäftigung mit der Aufgabenstellung fallen Begriffe und Konzepte, die Sie nicht nur in Ihrem Fachlexikon notieren, sondern die auch in vielfältiger Wei­se miteinander in Beziehung stehen. Diese Beziehungen lassen sich oft einfacher mit einem Fachklassen-Diagramm ausdrücken als durch bloße textuelle Beschreibung. Sie identifizieren also die zentralen Begriffe und stellen sie in Beziehung zueinander. Im Allgemeinen stellt ein Fachklassen-Diagramm nur eine andere Darstellung der In­

111

Problem verste­hen

Sammlung von Begriffen im Fachlexikon

Beziehungen im Diagramm dar­stellen

Page 120: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

formationen im Fachlexikon dar; jede Beziehung und jede Klasse im Diagramm soll­te auch im Fachlexikon aufgeführt und erklärt sein.

In unserem Beispiel sind die zentralen Begriffe Queue, Container und Element (wir wählen im Folgenden die englischen Begriffe, da sie oft kürzer sind und die Dia­gramme überschaubarer machen). Die zentralen Beziehungen sind sicherlich die KENNT-EIN-Beziehung zwischen einem Container und seinen Elementen34 sowie die IST-EIN-Beziehung zwischen der Queue und dem Container. Übertragen auf ein Klassendiagramm resultiert das Diagramm in Abbildung 29.

Während dieses Diagramm die erarbeiteten Beziehungen einigermaßen korrekt wie­dergibt35, haben wir bei der Modellierung eine Schnittstelle verwendet. In diesem Abschnitt wollen wir uns jedoch auf konkrete Klassen beschränken. Deshalb verwen­den wir ab jetzt ein etwas vereinfachte Modell (Abbildung 30).

34) Hier fragen Sie sich vielleicht, warum nicht auch eine HAT-EIN-Beziehung zwischen Container und dessen Elementen möglich ist. Kurz gesagt macht HAT-EIN als Ganzes-Teile-Beziehung (4.1.2) nur Sinn, wenn ein Element höchstens zu einem einzigen Container gehört (eine HAT-EIN-Beziehung ist immer hierarchisch). Da wir nicht verhindern wollen, dass Elemente durchaus ver­schiedenen Containern angehören können, modellieren wir die Beziehung zwischen einem Contai­ner und seinen Elementen als „normale“ KENNT-EIN-Beziehung.

35) Das Fachklassen-Lexikon legt nahe, dass die Beziehung zu den Elementen von Container ausge­hen sollte und nicht von Queue. Da hier aber Queue als konkreter Container die Art und Weise der Speicherung von Elementen bestimmen muss und Container als Schnittstelle überdies gar keine Elemente verwalten kann, wurde diese Beziehung der Queue-Klasse zugerechnet.

112

vereinfachtes Mo­dell

Abbildung 30: Vereinfachtes Fachklassen-Diagramm zur Queue-Aufgabe

Queue

add(e:Element)remove():ElementisEmpty():bool

Element0..*

elements

contains ►

Abbildung 29: Fachklassen-Diagramm zur Queue-Aufgabe

Queue

add(e:Element)remove():ElementisEmpty():bool

Element0..*

<<interface>>Container

add(e:Element)remove():ElementisEmpty():bool

<<realize>>

elementscontains ►

Page 121: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

4.4.3 Entwurfs-PhaseIn der Entwurfs-Phase wird das Modell aus der Analyse weiter verfeinert. Diese Ver­feinerung geht im Idealfall so weit, dass die Umsetzung des Entwurfs hinterher nur noch „ein Klacks“ ist. In der Regel werden erst im Entwurf Aufgaben den einzelnen Klassen zugeteilt und konkretes Verhalten spezifiziert.

4.4.3.1 TypenBisher haben wir uns kaum Gedanken um die Klasse Element gemacht. Jetzt stehen wir aber vor einem Problem. Im Idealfall möchten wir nämlich erreichen, dass unsere Queue Elemente beliebigen Typs enthalten kann, dabei aber Typ-sicher ist. Das be­deutet, dass wir gerne eine Queue hätten, die nur int-Werte enthält (wenn wir Zah­len in einer Queue speichern wollen), eine andere Queue soll nur Zeichenketten vom Typ string speichern, wieder eine andere Queue soll einzelne Zeichen vom Typ char speichern u. s. w.

Mit den uns bekannten Sprachmitteln lässt sich das nicht erreichen, die dazu erfor­derlichen Sprachkonzepte werden erst in Abschnitt 7.2 eingeführt. Wir müssen also uns auf einen Element-Typ festlegen. Wir wählen zu Demonstrationszwecken den Datentyp int, „verstecken“ ihn aber durch einen Typ-Alias namens Element, so dass wir die Klasse später leicht generisch (d. h. Typ-unabhängig) machen können.

4.4.3.2 VerhaltenIn unserem kleinen Beispiel haben wir bereits in der Analyse-Phase die Operationen der Klasse Queue bestimmt. Allerdings haben wir uns noch nicht konkret darüber Gedanken gemacht, wie die contains-Beziehung abgebildet wird. Objektorientier­te Programmiersprachen haben nämlich nur in seltensten Fällen Sprachmittel zur di­rekten Unterstützung von 0..*-Beziehungen. In der Regel können nur Zu-Eins-Bezie­hungen – also Beziehungen zu genau einem Objekt – problemlos realisiert werden. C++ bildet hier keine Ausnahme, so dass wir uns etwas geeignetes überlegen müs­sen.

Am Einfachsten ist es, wenn wir auf eine bestehende Lösung aufbauen könnten. Die C++-Standard-Bibliothek (8) bietet uns verschiedene Möglichkeiten, Objekte zu ver­walten. Wir wählen die Klasse list zur internen Speicherung der verwendeten Ob­jekte. Diese Klasse erlaubt es, auf einfache Weise Elemente „hinten“ hinzuzufügen und „vorne“ herauszunehmen. Man kann auch leicht überprüfen, ob ein list-Ob­jekt Elemente enthält.

4.4.3.3 ZuständeWir müssen uns noch detaillierter mit dem Verhalten unserer Queue beschäftigen. Zuerst haben wir noch nicht definiert, in welchem Zustand sich die Queue direkt nach der Erzeugung befindet. Enthält sie dann irgendwelche Objekte? Es ist klar, dass eine frisch erzeugte Queue am besten leer ist. Das scheint trivial, in größeren Projekten ist es aber unerlässlich, auch „Kleinigkeiten“ gewissenhaft zu notieren.

113

Problem lösen

Typen ergänzen

Verhalten detail­lieren

Zustände ausma­chen

Page 122: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

4.4.3.4 Fehler-SituationenAußerdem haben wir noch nicht darüber nachgedacht, was passieren soll, wenn die Methode remove auf eine leere Queue angewandt wird. Eigentlich soll sie das erste Element aus der Queue entfernen und zurückgeben, nur dass eine leere Queue kein erstes Element besitzt. Es handelt sich also um eine Fehlersituation, die geeignet si­gnalisiert und behandelt werden muss. C++ bietet die Möglichkeit, bei einem Fehler den normalen Programmfluss zu unterbrechen und ein Fehlerobjekt an den Aufrufer auf eine bestimmte Weise zurückzugeben, so dass

• dieser weiß, dass ein Fehler aufgetreten ist,

• dieser weiß, welcher Fehler aufgetreten ist und

• er diesen geeignet behandeln kann (und zwar möglichst unmittelbar nach dem Auftreten des Fehlers).

Diese Art der Fehlerbehandlung werden Sie detaillierter in Kapitel 5 kennen lernen. Wir halten an dieser Stelle fest, dass bei einer remove-Nachricht an eine leere Queue ein Objekt der Klasse RemoveOnEmptyQueue generiert wird, die wir na­türlich in unseren Entwurf integrieren müssen.

4.4.3.5 Resultierendes KlassendiagrammDamit wäre im Entwurf alles getan. Die Aufgaben sind klar verteilt – die Queue de­legiert die Nachrichten, entsprechend verändert, an die Klasse list. Demnach er­halten wir das Diagramm in Abbildung 31. Dabei modellieren wir list bewusst nicht als Klasse, sondern als Attribut. Näheres hierzu finden Sie im nachfolgenden Abschnitt.

Der gestrichelte Pfeil zwischen Queue und RemoveOnEmptyQueue zeigt übri­gens eine Abhängigkeit an. Queues besitzen zwar keine Objekte der Klasse Remo­veOnEmptyQueue, und sie unterhalten auch keine sonstigen Beziehungen zu sol­chen Objekten, aber sie können Objekte dieser Klasse erzeugen. Das macht sie ebenfalls zu Klienten der Klasse RemoveOnEmptyQueue, und diese Abhängigkeit ist hier mit modelliert worden. Der Stereotyp <<creates>> weist deutlich auf die Art der Abhängigkeit hin.

114

Fehler-Situatio­nen entdecken

überarbeitetes Klassendiagramm

Abhängigkeiten darstellen

Abbildung 31: Klassendiagramm nach der Entwurfsphase

Queue

add(e:Element)remove():ElementisEmpty():bool

elements: list Element0..*

elements

contains ►

RemoveOnEmptyQueue<<creates>>

Page 123: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

4.4.4 Exkurs: Attribute und kompositionale BeziehungenIm letzten Beispiel haben Sie gesehen, dass list nicht per Assoziation mit der Klasse Queue verbunden wurde, sondern als Attribut eingebunden ist. Überhaupt werden Sie sich fragen, wozu Attribute denn gut seien. Denn schließlich ist in einem objekt­orientierten System alles ein Objekt, und jedes Objekt gehört einer Klasse an, oder? Somit kann man folgern, dass keine Attribute notwendig seien und alles über Asso­ziationen abgebildet werden kann, oder?

Die Antwort auf diese Fragen ist Übersichtlichkeit. Letztlich enthalten Objekte ir­gendwie „fundamentale“ Daten wie Zahlen, Zeichen, Zeichenketten oder Wahrheits­werte. Es ist richtig, dass in einer rein objektorientierten Welt beispielsweise jede Zahl ein eigenes Objekt der entsprechenden Zahlen-Klasse ist. Es ist jedoch so, dass wenn für jede solche Beziehung zu einer derart fundamentalen Klasse eine Bezie­hung im Diagramm eingezeichnet werden müsste, die Diagramme sehr bald sehr voll würden. Wenn eine Klassen z. B. fünf Attribute von jeweils drei verschiedenen Ty­pen hat, müssten zusätzlich zum Klassen-Kasten noch drei weitere Kästen und fünf weitere Pfeile gezeichnet werden. Die Notation von Attributen für Objekte von sol­chen „fundamentalen“ Typen verkleinert die Diagramme also erheblich.

Das Charakteristische an Attributen ist, dass sie außerhalb ihres Objekts nicht existieren können. Diese Art von Beziehung haben wir noch nicht kennen gelernt: Es handelt sich dabei um die Komposition, die eine strengere Form der Aggregation ist. Attribute kön­nen also (mit allen oben genannten Nachteilen) als kompositionale Beziehungen model­liert werden. Abbildung 32 zeigt, wie das obige Klassendiagramm nach der Änderung von Attributen zu Kompositionen aussieht.

4.4.5 Implementierungs-PhaseJetzt kommen wir zur Umsetzung des Entwurfs in C++ (endlich!) Zuerst müssen wir die Syntax von C++ für das Definieren von Klassen, Operationen und Methoden kennen lernen.

4.4.5.1 Klassen und MethodenIn C++ hat eine (einfache) Klasse ungefähr folgenden Aufbau:

class Name

115

Attribute oder Be­ziehungen?

Attribute sind übersichtlicher bei fundamenta­len Typen

Kompositionen sind strenge Ag­gregationen

Klassen-Defini­tionen in C++

Abbildung 32: Komposition anstatt von Attributen

Queue

add(e:Element)remove():ElementisEmpty():bool

Element0..*

elements

contains ►list1

RemoveOnEmptyQueue

<<creates>>

Page 124: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

{[Zugriffs-Modifizierer1 :]

Element-Deklaration1Element-Deklaration2...

[Zugriffs-Modifizierer2 :]

Element-Deklaration3Element-Deklaration4...

};Am Anfang steht der Klassen-Kopf, der (zuerst) nur aus dem Namen der Klasse be­steht. Nach der folgenden öffnenden geschweiften Klammer folgt der Klassen-Kör­per, der aus Deklarationen von Elementen besteht, wobei unter Elementen hier Ope­rationen, Attribute und Typen gemeint sind. Optional kann solchen Deklarationen ein Zugriffs-Modifizierer vorangestellt werden. Mehr dazu erfahren Sie in Abschnitt 4.4.6.

Wenn ein Zugriffs-Modifizierer gänzlich fehlt, wird private angenommen. Beispiel:

1 class Flugzeug2 {3 void hebAb ();4 };

Die Methode hebAb ist privat und kann von Objekten anderer Klassen nicht verwendet werden.

Operations- und Attribut-Definitionen unterscheiden sich nicht von gewöhnlichen Funktions- bzw. Variablen-Deklarationen. Einen Unterschied gibt es bei den Attribu­ten aber dennoch: sie dürfen nicht innerhalb der Klassendefinition initialisiert wer­den. Zur Initialisierung muss ein sogenannter Konstruktor definiert werden, dazu aber später mehr (4.5).

Erzeugen Sie für dieses Beispiel unbedingt ein eigenes Projekt, da es aus mehreren Da­teien bestehen wird. Nennen Sie das Projekt queue1, weil Sie in den Übungen noch eine zweite Version der Queue-Aufgabe entwickeln werden. Und Sie wollen natürlich auch alle Übungen lösen, oder?

4.4.5.2 Definition der Klassen und SchnittstellenZuerst definieren wir die Schnittstelle der Klasse Queue, damit andere Module dar­auf zugreifen können. Wir fangen also mit der Header-Datei an. Tippen Sie Folgen­des in eine Datei mit dem Namen queue.h ein:

1 /*** Beispiel queue1/queue.h ***/2 #include <list>3 using namespace std;4

116

Klassen-Kopf und Klassen-Körper

Definition der Klasse

Elemente sind standardmäßig privat!

Page 125: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

Zuerst der übliche „Programm-Kopf“. Beachten Sie, dass eine neue Header-Datei eingebunden wird. <list> enthält die Definition der Klasse list, die wir weiter hinten benötigen.

5 typedef int Element;Hier definieren wir den Typ Element als Alias zu int.

6 class Queue7 {

Der Klassen-Kopf.8 public :

Damit drücken wir aus, dass die folgenden Elemente der Klasse öffentlich zugäng­lich sind und somit zur öffentlichen Schnittstelle der Klasse gehören (4.4.6).

9 // stellt "element" ans Ende der Schlange10 void add (Element element);1112 // entfernt das erste Element aus der Schlange und gibt es zurück; wirft ein Objekt der13 // Klasse RemoveOnEmptyQueue aus, falls die Methode für eine leere Queue aufgerufen

wird14 Element remove ();1516 // liefert true zurück, wenn die Queue keine Elemente enthält, false sonst17 bool isEmpty () const;

Hier definieren wir die Operationen, welche die Klasse anbietet. Wenn die Schreib­weise so aussieht wie für „normale“ Funktionen, dann wird dadurch auch gleich aus­gedrückt, dass an anderer Stelle eine passende Methode dafür existiert. Die Syntax für abstrakte Operationen sieht etwas anders aus, diese lernen Sie in Abschnitt 4.6.1 kennen.

Neu für Sie ist das const hinter der Deklaration einer Operation. Es bedeutet, dass das Objekt, für das die Methode aufgerufen ist, zum Zeitpunkt der Ausführung wie ein const-Objekt behandelt wird. Das bedeutet im Klartext, dass eine solche Me­thode das Objekt nicht verändern darf. Mehr dazu finden Sie in Abschnitt 4.4.7.1.

18 private :Die öffentliche Schnittstelle ist abgeschlossen, jetzt kommen interne Details der Klasse, die nicht nach außen sichtbar sein sollen.

19 list<Element> elements;

Unsere die Elemente verwaltende Liste. Die seltsame Syntax mit den spitzen Klam­mern hat ihren Ursprung darin, dass list eine generische Klasse oder Schablone ist, die mit vielen Typen funktioniert. Durch <Element> wird dem Übersetzer mit­geteilt, dass wir eine Liste aus Element-Elementen definieren und benutzen wollen. Mehr zu Schablonen erfahren Sie in Abschnitt 7.2.

20 };21

Das Ende der Klassendefinition.22 class RemoveOnEmptyQueue23 {

117

const-Operatio­nen

Page 126: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

24 };

Unsere die Ausnahme repräsentierende Klasse. Dies ist eine leere Klasse – sie enthält weder Attribute noch Operationen noch Methoden. Dennoch ist sie an dieser Stelle völlig ausreichend; warum das so ist, werden Sie in Abschnitt 5 verstehen.

Damit ist unsere Header-Datei abgeschlossen, und wir können uns dem Programm-Code zuwenden.

Wenn Sie sich in der Programmiersprache Java auskennen, werden Ihnen jetzt sicher ei­nige Unterschiede zwischen Klassen-Deklarationen in Java und in C++ aufgefallen sein:

• In Java ist für „öffentliche“ Klassen ein public vor der Klassendefinition not­wendig. In C++ sind alle Klassen öffentlich; will man Klassen verstecken, muss man sie in einem anonymen Namensraum (8.2) definieren.

• Die Klassen-Definition in C++ endet mit einem Semikolon, in Java nicht.

• In C++ werden die Methoden (in der Regel) außerhalb der Klasse definiert, in Java gibt es nur die Möglichkeit der Definition innerhalb der Klasse.

• In C++ können sich Elemente einen Zugriffs-Modifizierer teilen, in Java muss der Zugriffs-Modifizierer vor jeder Definition stehen. Weiterhin endet der Zugriffs-Modifizierer in C++ mit einem Doppelpunkt, in Java nicht.

• Wie oben erwähnt, können in C++ die Attribute nicht innerhalb der Klassendefini­tion initialisiert werden, in Java aber schon.

• Eine Datei, die eine Klasse enthält, muss in C++ nicht genauso wie die Klasse hei­ßen, in Java schon.

4.4.5.3 Implementierung der OperationenJetzt kommen wir zu der Implementierung der Operationen der Klasse, den Metho­den. In einer neuen Datei tippen Sie bitte folgenden Programm-Code in eine Datei namens queue.cpp ein:

1 /*** Beispiel queue1/queue.cpp ***/2 #include "queue.h"3

Beachten Sie, dass wir die Header-Datei queue.h einbinden, um die Definition der Klasse verfügbar zu machen.

4 void Queue::add (Element e)5 {6 elements.push_back (e);7 }8

Die Implementierung der Operation add. Bis auf den ::-Operator sollten Sie nichts ungewöhnliches feststellen. Der ::-Operator erlaubt es, bei der Verwendung eines Namens explizit einen Gültigkeitsbereich zu spezifizieren. In diesem Fall wollen wir die Methode add der Klasse Queue definieren, sind aber nicht mehr im Gültigkeits­bereich der Klasse. Durch Queue:: wird der nachfolgende Name add vom Über­setzer innerhalb der Definition der Klasse Queue gesucht. Queue::add wird auch qualifizierter Bezeichner genannt.

118

Unterschiede zur Java-Klassendefi­nition

Definition der Methoden

::-Operator

Page 127: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

Zum Gültigkeitsbereich: Jede Klasse bildet einen eigenen, kleinen Gültigkeitsbe­reich. Ein Name, der innerhalb einer Klassendefinition durch eine Deklaration einge­führt wird, ist also nur innerhalb dieser Klasse sichtbar. Das bedeutet, dass Sie sich nicht darüber Gedanken machen müssen, dass in zwei verschiedenen Klassen eine Operation denselben Namen hat: Da beide in verschiedenen Gültigkeitsbereichen lie­gen, ist das überhaupt kein Problem. Die Folge dieser Regelung ist aber, dass gele­gentlich der Gültigkeitsbereich explizit mit Hilfe des ::-Operators (s. o.) angegeben werden muss, damit der Übersetzer einen verwendeten Namen richtig zuordnen kann.

Sie lernen hier auch noch einen weiteren Operator kennen: den Elementzugriffs-Ope­rator . (Punkt!) Dieser Operator wird Ihnen in der objektorientierten (C++-)Welt ständig begegnen. Er ist nämlich derjenige Operator, der es Ihnen erlaubt, eine Nach­richt an ein Objekt zu schicken.

Schauen Sie sich einmal die Zeile 6 genauer an: elements.push_back (e) be­deutet, dass dem Objekt elements die Nachricht push_back mit e als Argument geschickt wird. Dies führt dazu, dass eine Methode namens push_back für dieses Objekt aufgerufen wird. Ein weiteres Beispiel finden Sie in Zeile 16 (s. u.), dort gleich zweimal. Zuerst wird dem elements-Objekt die Nachricht begin ge­schickt. Das Ergebnis wird das Argument der Nachricht erase, die ebenfalls an das elements-Objekt gesendet wird. Die ganze Logik der objektorientierten Program­mierung liegt also im Verschicken der richtigen Nachrichten mit richtigen Argumen­ten an die richtigen Objekte zur richtigen Zeit.

Preisfrage: Was für ein elements-Objekt wird in Zeile 6 als Ziel für die push_back-Nachricht benutzt? Na, das aus der Klassendefinition in Zeile 19, wer­den Sie antworten. Ja, aber es kann doch viele Objekte zu einer Klasse geben, wobei jedes ein „eigenes“ elements-Objekt besitzt. Woher weiß denn die Methode, wel­ches zu benutzen ist?

Ich kann Sie beruhigen: Alles hat seine Ordnung. Immer wenn Sie eine Nachricht an ein Objekt schicken, wird an die zugehörige Methode auch das Objekt übergeben, für das sie aufgerufen wurde. Dieses Objekt wird jedoch nirgends deklariert und ist so­zusagen „versteckt“. Es gibt in C++ das Schlüsselwort this, das einen Zeiger (3.4.3.3) auf das Objekt zurückgibt, für das eine Methode aufgerufen ist. Dieser Zei­ger wird vom Übersetzer automatisch benutzt, wenn auf Objekt-eigene Attribute zu­gegriffen wird.

Wenn Sie das obige Beispiel betrachten, sehen Sie, dass an das elements-Attribut die Nachricht push_back geschickt wird. Innerhalb der Implementierung dieser Methode verweist this auf das Objekt elements. Wenn nun die Klasse list ein Attribut namens tail enthält und die Methode push_back darauf zugreift, wird konkret auf elements.tail zugegriffen, weil die Nachricht an das elements-Objekt geschickt wurde.

119

eine Klasse – ein Gültigkeitsbe­reich

Zugriff auf Klas­senelemente über den .-Operator

das eigene Ob­jekt: this

Page 128: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

Generell wird immer this verwendet, wenn Sie beim Zugriff auf Attribute und beim Aufruf von Methoden kein Objekt angeben. Die obige Funktion könnte man also folgendermaßen umschreiben:

4 void Queue::add (Element e)5 {6 this->elements.push_back (e);7 }8

Dabei ist der Operator -> eine besser lesbare Kombination aus Dereferenzierung (*) und Element-Zugriff (.); eine weitere Alternative wäre folglich

4 void Queue::add (Element e)5 {6 (*this).elements.push_back (e);7 }8

wobei die Klammern auf Grund der Prioritätsregeln (3.4) notwendig sind.

Jetzt haben wir uns aber genug mit der Methode add aufgehalten. Gehen wir zur nächsten Methode über.

9 Element Queue::remove ()10 {11 // prüfe ob Queue leer ist12 if (isEmpty ())13 throw RemoveOnEmptyQueue ();1415 Element result = elements.front ();16 elements.erase (elements.begin ());17 return result;18 }19

Hier werden Sie das erste Mal mit C++-Ausnahmen konfrontiert. Zeile 12 prüft, ob die Queue leer ist, denn dann ist ein Entfernen des ersten Elements ein Fehler. In ei­nem solchen Fall wird ein throw-Ausdruck ausgewertet. Stellen Sie sich throw als einen Operator vor, der ein Objekt als Operanden hat. Dieses Objekt ist hier von der Klasse RemoveOnEmptyQueue; über das Erzeugen von Objekten einer Klasse handelt Abschnitt 4.5. Ein solches an throw übergebene Objekt wird zum Aufrufer (oder dessen Aufrufer oder zum Aufrufer dessen Aufrufers u. s. w. ) weitergereicht, bis sich jemand findet, der die Ausnahme behandelt. Details zu diesem Vorgang und wie man solche Ausnahmen auch tatsächlich behandelt, finden Sie in Abschnitt 5. Für Sie ist hier nur wichtig zu wissen, dass der Rest der Funktion (Zeilen 15-17) nicht ausgeführt werden, nachdem der throw-Ausdruck ausgewertet worden ist.

Der Rest der Funktion greift auf die Schnittstelle der Klasse list zu. front gibt das erste Element der Liste zurück, begin liefert einen Iterator (eine Art Verweis) auf das erste Element in der Liste, und erase löscht ein Element aus der Liste, das über einen Iterator referenziert wird. Nichts Weltbewegendes. Aber nochmals zur Er­innerung: isEmpty() entspricht this->isEmpty() und elements entspricht this->elements. Es gibt zur Programm-Laufzeit kein Attribut und keine Metho­de ohne zugehöriges Objekt. Punkt. Aus. Basta.

120

der Operator ->

Auswerfen von C++-Ausnahmen

Page 129: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

20 bool Queue::isEmpty () const21 {22 return elements.empty ();23 }

Die Funktion ist genauso einfach wie add. Die Nachricht wird einfach an das ele­ments-Objekt weitergereicht, dasselbe geschieht mit dem Ergebnis. (Informationen zu dem const-Schlüsselwort in der Deklaration finden Sie in Abschnitt 4.4.7.1).

So, alle Methoden der Klasse Queue sind definiert. Damit wären wir in dieser Datei am Ende angelangt. Jetzt geht es darum, ein paar Objekte der Klasse zu testen und zu schauen, ob die Klasse auch das tut, was von ihr verlangt ist. Vorher aber schauen wir uns an, was C++ an Schutz gegenüber „bösen“ Klienten bietet...

4.4.6 Exkurs: ZugriffsschutzSie haben erfahren, dass Objekte gekapselt sind und ihre Daten vor anderen Objekten verstecken. Dieses Verstecken ist eine gute Sache. Je weniger Klienten von den spe­ziellen Eigenheiten eines Dienstleisters wissen – und Attribute eines Objekts gehören dazu – desto kleiner ist die Kopplung zwischen Klient und Dienstleister und desto einfacher kann ein Objekt seine Implementierung ändern, ohne dass dies Klienten ir­gendwie stört (sie merken es ja nicht, solange die Schnittstelle dieselbe bleibt). Au­ßerdem kann es sein, dass das Objekt sensible Daten enthält – etwa ein Passwort zur Autorisierung bei einem anderen Objekt.

Es macht oft auch Sinn, Operationen zu verstecken, wenn diese Methoden Klienten nicht zur Verfügung gestellt werden sollen. Beispielsweise wird ein Objekt, das eine Schnittstelle zum Lösen eines Gleichungssystems anbietet, vielleicht nur eine Nach­richt verstehen: löse. Ist der Algorithmus zum Lösen jedoch relativ komplex, macht es Sinn, ihn in mehrere Teile aufzuspalten. In der prozeduralen Programmierung wird man den Algorithmus in mehrere Funktionen oder Prozeduren aufspalten. In der objektorientierten Welt gibt es stattdessen Methoden, die man aber Klienten nicht zur Verfügung stellen möchte, weil sie den internen Zustand des Objekts so manipulie­ren, dass er zwischenzeitlich vielleicht inkonsistent ist. In einem solchen Fall muss man die entsprechenden Operationen vor potentiellen Klienten verstecken.

Die Lösung hierfür ist einfach, verschiedene Sichten auf die vorhandene Schnittstelle anzubieten. Für Objekte völlig fremder Klassen existiert die öffentliche Sicht, die in C++ durch den Zugriffs-Modifizierer public dargestellt wird. Nur Elemente, die innerhalb der Klassendefinition in einem public-Bereich deklariert werden, sind für diese Objekte sichtbar.

public sollte nie für Attribute verwendet werden, weil dies dem Prinzip der Kapse­lung (4.1.4) zuwiderläuft. Attribute sollten immer als „Geheimnis“ des Dienstleisters angesehen werden und in ständiger Gefahr, geändert und entfernt zu werden. Nur die operationale Schnittstelle eines Objekts ist stabil. Außerdem kann ein Klient durch Manipulation eines fremden Attributs die Konsistenz der Daten dieses Objekts zer­stören, so dass der innere Zustand des Objekts „in die Dutten“ geht. Nur die Metho­den des Objekts stellen sicher, dass der Objekt-Zustand konsistent bleibt.

121

Attribute sind im­mer privat

Operationen sind manchmal privat

verschiedene Sichten auf Schnittstellen

Nie public-At­tribute verwen­den!

Page 130: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

Merksatz 14: Verwende nie öffentliche Attribute!

Für Objekte innerhalb derselben Vererbungshierarchie existiert der Zugriffs-Modifi­zierer protected. Alle protected-Elemente sind von erbenden Klassen aus zu­greifbar; für nicht erbende Klassen hingegen sind sie ebenfalls privat. protected sollte nur für Operationen und nicht für Attribute verwendet werden.

Soll ein Element nur für Objekte der eigenen Klasse zugreifbar sein, so muss es in­nerhalb der Klasse in einem private-Abschnitt definiert werden. Alle Attribute sollten generell privat gemacht werden.

C++ bietet nicht die Möglichkeit, den Zugriffsschutz auf Objektebene zu spezifizieren, bloß auf Klassenebene. Das bedeutet, dass ein Objekt einer bestimmten Klasse X Zu­griff auf die privaten Elemente aller anderen Objekte derselben Klasse X hat, nicht nur auf seine eigenen. Die direkte Manipulation von Attributen anderer Objekte derselben Klasse ist nicht besonders schön und sollte man nach Möglichkeit vermeiden. Besser ist es, zum Zugriff entsprechende private Operationen anzubieten.

Wichtig ist, dass Sie sich an dieser Stelle vergegenwärtigen, dass versteckte Opera­tionen und (versteckte) Methoden zwei verschiedene Paar Schuhe sind. Operationen können Sie – wie oben beschrieben – mit Hilfe der Zugriffsmodifizierer public und private potentiellen Klienten bekannt machen bzw. vor potentiellen Klienten verstecken, so dass diese entsprechende Nachrichten an entsprechende Objekte ver­schicken können oder eben nicht. (Dies stellt der Übersetzer sicher.) Methoden hin­gegen (also die Implementierungen von Operationen) sind immer versteckt in dem Sinne, als dass potentielle Klienten nicht wissen, wie eine Operation tatsächlich im­plementiert ist. Das ist aber unabhängig vom Zugriffsmodifizierer der Operation: Selbst die Implementierung einer öffentlichen (= public) Operation ist potentiellen Klienten unbekannt. Methoden sind nur innerhalb der enthaltenden Klasse bekannt (und dort natürlich auch nicht „versteckt“).

4.4.7 Exkurs: const-Operationen und const-Parameter

4.4.7.1 const-OperationenOperationen lassen sich nach der Art des Zugriffs auf „ihr“ Objekt in drei verschie­dene Gruppen einteilen:

(1) konstruierende Operationen: Sie erstellen ein Objekt.

(2) modifizierende Operationen: Sie verändern ein bestehendes Objekt.

(3) beobachtende Operationen: Sie liefern Informationen zum Objekt, verändern es jedoch nicht.

Diese Einteilung ist sinnvoll, weil sie den Blick auf die Schnittstelle eines Objekts noch einmal erweitern. Wir haben im letzten Abschnitt erfahren, dass es durch die Zugriffs-Modifizierer public, protected und private möglich ist, den Zu­griff auf Operationen einer Klasse zu regeln. Doch diese Modifizierer erlauben nur eine „Ganz-oder-gar-nicht“-Strategie. Entweder ist die Operation für einen Klienten verfügbar oder nicht.

122

drei Arten von Operationen

Page 131: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

Oft ist dies aber nicht ausreichend. Denn manchmal möchte man verhindern, dass ein Objekt von einem Programmteil (einer Funktion, einer Methode) verändert wird. Beispielsweise wollen Sie bestimmt nicht, dass die Methode isEmpty der Klasse Queue die Queue verändert; sie wollen ja nur erfragen, ob sie Elemente enthält. Die Methode isEmpty ist nach der obigen Klassifikation eindeutig eine beobachtende Operation und soll das Objekt nicht verändern. Diese Garantie der Nicht-Verände­rung kann man in C++ explizit bei der Deklaration (und Definition) einer Operation bzw. Methode durch das const-Schlüsselwort ausdrücken:

16 bool isEmpty () const;Ein Dienstleister (wie diese Methode) sagt durch das Schlüsselwort const aus: „Du kannst dir sicher sein, dass ich das Objekt nicht verändern werde, für das ich aufge­rufen wurde“. Dabei sind das nicht nur leere Worte: Der Übersetzer prüft sehr genau, ob die Methode das Objekt dennoch zu verändern versucht, und meldet in einem sol­chen Fall einen Fehler.

Merksatz 15: Verwende const bei beobachtenden Operationen!

Eine direkte Konsequenz daraus ist, dass eine beobachtende Methode (= mit const) nicht auf ihrem eigenen Objekt modifizierende Methoden (= ohne const) ausführen kann. Es ist ja auch unsinnig zu behaupten: „ich werde das Objekt nicht verändern“, und dann jemand anderes zu beauftragen, diese Veränderung durchzuführen. Einmal beobachtend, immer beobachtend; einmal const, immer const. Das stellt eben­falls der Übersetzer sicher.

Eine weitere Konsequenz ist, dass für echte const-Objekte (die also in der Definiti­on das const-Schlüsselwort enthalten, s. Abschnitt 3.4.4), ebenfalls nur const-Operationen aufgerufen werden dürfen. Denn sonst wäre folgender, ziemlich gefähr­licher Code erlaubt:

1 /*** Beispiel const1.cpp ***/23 // Diese Klasse kapselt einen Wert.4 class Wert5 {6 public :7 // Konstruktor initialisiert das Objekt und setzt den Startwert (s. 4.5.1)8 Wert (int startwert);910 // gibt den gespeicherten Wert zurück11 int gibWert () const;1213 // ändert den Wert14 void setzeNeuenWert (int neuerWert);1516 private :17 int wert;18 };1920 // Definition eines Konstruktors (s. 4.5.1)21 Wert::Wert (int startwert)22 :23 wert (startwert)

123

Änderung verhin­dern mit const

einmal const, immer const

Methoden und konstante Objekte

Page 132: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

24 {25 }2627 int Wert::gibWert () const28 {29 return wert;30 }3132 void Wert::setzeNeuenWert (int neuerWert)33 {34 wert = neuerWert;35 }3637 int main ()38 {39 // ein konstanter Wert40 const Wert dieAntwort (42);41 // Konstante wird verändert?!? Kein gültiges C++!42 //dieAntwort.setzeNeuenWert (24);43 return 0;44 }

In C++ führt die – auskommentierte – Zeile 42 (glücklicherweise!) zu einem Fehler und verhindert die Veränderung eines konstanten Objekts.

4.4.7.2 const-Parameter

const ist aber auch im Zusammenhang mit Referenzen nützlich, insbesondere bei der Parameterübergabe. Sie kennen bisher zwei Arten der Parameterübergabe (3.6.4):

(1) Übergabe per Wert, wenn die Funktion lediglich den Wert benötigt und die ent­haltende Entität nicht verändern will

(2) Übergabe per Referenz, wenn die Funktion die übergebene Entität verändern will

Jetzt lernen Sie eine dritte Variante kennen:

(3) Übergabe per const-Referenz: wenn die Funktion die übergebene Entität nicht verändern will, jedoch eine Kopie einsparen will

Erinnern Sie sich? Bei der Wertübergabe wird der zu übergebende Wert in den Para­meter der Funktion kopiert. Damit wird schließlich sichergestellt, dass das ursprüng­liche Objekt nicht verändert wird. Bei größeren Objekten kann diese Kopiererei ziemlich viel Aufwand (bezogen auf Zeit und Raum) bedeuten. Bei Referenzen ent­fällt hingegen die Kopie (aus verständlichen Gründen); stattdessen wird ein Verweis auf das ursprüngliche Objekt übergeben. Dieser Verweis wird natürlich ebenfalls ko­piert, hat aber immer dieselbe (kleine) Größe, während das zugehörige Objekt durch­aus sehr groß sein kann.

Die dritte Variante vereint nun die Vorteile beider Welten: Das Objekt wird nicht ko­piert, dennoch ist die Funktion nicht in der Lage, das Objekt zu verändern, weil es in­nerhalb der Funktion als Konstante behandelt wird. Beispiel:

1 /*** Beispiel const2.cpp ***/2 #include <ostream>3 #include <iostream>4 #include <string>5 using namespace std;

124

Übergabe per const-Referenz

Kopieren vermei­den

Page 133: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

67 // gibt „message“ aus8 void print (const string &message) // const-Referenz!9 {10 cout << message << endl;11 }1213 int main ()14 {15 string message (16 "Dies ist eine sehr lange Zeichenkette, die sehr viele "17 "Zeichen enthält. Eine Zeichenkette dieser Länger bei "18 "einem Funktionsaufruf zu kopieren dauert nicht "19 "besonders lange, aber stellen Sie sich vor, diese "20 "Funktion wird sehr oft aufgerufen (z. B. 100000 Mal). "21 "Dann werden Sie sicherlich einen Unterschied in der "22 "Geschwindigkeit spüren."23 );24 for (int i = 0; i < 100000; ++i)25 print (message);26 return 0;27 }

Die Funktion print in diesem Beispiel kann den Parameter message nicht verän­dern, weil es als Referenz auf ein const-Objekt definiert wurde. Jeder Versuch ei­ner Änderung führt zu einer Fehler bei der Übersetzung.

Wenn Sie die Zeile 8 abändern zu:8 void print (string message)

wird das Programm weiterhin übersetzbar sein und funktionieren, allerdings etwas langsamer, weil bei jedem Aufruf der Funktion print eine Kopie der gesamtem Zeichenkette gemacht wird.36 Führen Sie mal den folgenden Versuch durch:

(1) Löschen Sie die Zeile 10, in der die Ausgabe auf den Bildschirm geschieht, da­mit das Programm beim Testlauf nicht ewig braucht (die Ausgabe ist der lang­samste Teil in unserem Programm).

(2) Übersetzen Sie das Programm einmal mit Wert- und einmal mit Referenz-Über­gabe und lassen Sie beide mehrmals laufen

(3) Vergleichen Sie die Laufzeiten beider Programme.

Sie werden feststellen, dass das Programm mit der Referenz-Übergabe wesentlich schneller läuft. (Falls Sie keinen oder nur marginale Unterschiede bemerken, erhöhen Sie die Anzahl der Schleifendurchläufe in Zeile 24, etwa auf das Zehnfache.)

Auf meinem PC mit einem AMD Athlon 2600-Prozessor (~ 1800 MHz Taktfrequenz) wurden beide Programme mit VC++ 6.0, Service Pack 5 und den Standard-Projekt-Ein­stellungen übersetzt. Das Programm mit Referenz-Übergabe benötigte bei 100000 Schleifendurchläufen zwischen 15 und 31 Millisekunden, das mit Wert-Übergabe zwi­schen 78 und 91 Millisekunden. Somit war das Programm mit Referenz-Übergabe etwa fünfmal schneller!

36) Fairerweise muss man sagen, dass dies sehr von der Implementierung der Klasse string ab­hängt. Intelligente Implementierungen vermeiden durch Techniken wie Referenzzähler unnötiges Kopieren der enthaltenen Daten, somit ist das obige Beispiel in solchen Fällen wesentlich weniger „eindrucksvoll“.

125

Übergabe per Re­ferenz ist effizien­ter

Page 134: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

Aus diesem Grund werden größere Objekte in C++-Programmen häufig als const-Referenz übergeben, wenn sie nicht verändert werden sollen. Bei fundamentalen Da­tentypen (int, char u. s. w.) lohnt sich die Übergabe per const-Referenz in der Regel jedoch nicht, weil die Datenmenge so klein ist, dass keine Einsparung mehr möglich ist.

Sie können konstante Werte an eine Funktion mit einer const-Referenz übergeben:

1 /*** Beispiel const3.cpp ***/2 #include <ostream>3 #include <iostream>4 using namespace std;5 void gibAus (const int &wert)6 {7 cout << wert << endl;8 }9 int main ()

10 {11 gibAus (1);12 return 0;13 }

Dies funktioniert, obwohl 1 eine Konstante und kein Objekt in C++ ist. C++ definiert in diesem Fall, dass eine temporäre „Variable“ vom Typ const int erzeugt wird. Die Funktion gibAus bekommt dann einen Verweis auf diese Variable übergeben. Die temporäre Variable wird dann nach der Benutzung (d. h. nach Aufruf der Funktion) au­tomatisch zerstört. Zu temporären Objekten siehe auch Abschnitt 4.5.4.

4.4.8 Test-PhaseSie haben nun Ihre erste C++-Klasse programmiert. Spätestens jetzt müssen Sie sich geeignete Testfälle ausdenken, um zu überprüfen, ob die Klasse Ihren Anforderun­gen entspricht.

In der Software-Entwicklung hat sich die Einsicht durchgesetzt, dass sich in jedem nicht-trivialen Programm Fehler befinden. Um diese möglichst frühzeitig finden und entfernen zu können, wurde die Methode der Test-getriebenen Entwicklung aus der Tau­fe gehoben. Diese Methode verfolgt die Philosophie, dass Testfälle so früh wie möglich erstellt werden sollen, in der Regel noch bevor die erste Zeile Programm-Code geschrie­ben wird. Ist der Programm-Code dann entwickelt, wird sofort getestet, und Fehler wer­den sehr schnell erkannt und ausgebaut. Die (eher traurige) Einsicht hinter dieser Philo­sophie ist diejenige, dass Programmierer generell zu faul sind, um Ihre Software vernünftig zu testen: Sobald sie das Gefühl haben, dass das Programm einen guten Ein­druck macht, wenden Sie sich der nächsten Aufgabe zu. Das hat jedoch meistens fatale Folgen.

Zum Entwickeln der benötigten Tests stehen stehen heutzutage in vielen Programmier­sprachen leistungsfähige Frameworks zur Verfügung, etwa JUnit37 in Java oder Cpp­Unit38 in C++.

Merksatz 16: Teste viel und ausführlich!

37) s. http://www.junit.org/ und http://junit.sourceforge.net/38) s. http://cppunit.sourceforge.net/

126

Testen ist wich­tig!

Konstanten und const-Referen­zen

Page 135: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

Die Testfälle lassen sich aus der Entwurfs- und sogar aus der Analyse-Phase ableiten. In unserem Fall müssen wir die besonderen Eigenschaften des Queue-Containers tes­ten. Praktisch heißt das, dass wir mehrmals eine Queue erzeugen, verschiedene Ob­jekte hineintun und überprüfen, dass sie in der richtigen Reihenfolge wieder heraus­kommen.

Das Entwickeln guter Tests ist nicht so einfach, wie es aussieht. Ein guter Test ist einer, der Fehler findet. Diese zu entwickeln bedeutet, sich intensiv mit der Schnittstelle und Funktionsweise der zu testenden Einheit auseinander zu setzen, Grenzwerte und Invari­anten zu erarbeiten, Zustandsübergänge bei zustandsbehafteten Klassen zu überprüfen u. s. w. Es gibt inzwischen sogar eigene Lehrgänge mit abschließendem Zertifikat, die nur das Testen objektorientierter Systeme zum Thema haben. Auf Grund der Komplexi­tät der Materie gehen wir im weiteren Verlauf des Skripts nicht tiefer auf das Thema Testen ein.

Ein Testfall besteht immer aus einer Beschreibung dessen, was getestet werden soll, und dem erwarteten Ergebnis. Letzteres wird häufig Erwartungswert oder Soll-Wert genannt. Beim Testen vergleicht man dann das tatsächliche Ergebnis, den Ist-Wert, mit dem Soll-Wert; werden Abweichungen festgestellt, so liegt ein Fehler vor. Dies kann ein Fehler in der getesteten Einheit (Methode, Funktion etc.) sein, aber auch ein Fehler im Test selbst. Das gilt es dann herauszufinden, den Fehler zu korrigieren und dann alle Tests erneut durchzuführen, um sicherzugehen, dass der Fehler eliminiert wurde und keine weiteren Fehler durch die Korrektur eingebaut wurden. Tests, die bei Änderungen der Software durchlaufen werden, nennt man auch Regressionstests.

Wir wollen folgende Tests durchführen (jeder Test beginnt mit einem neuen, leeren Objekt!):

(1) isEmpty() == true(2) add(1);

isEmpty() == false(3) add(1);

remove() == 1; isEmpty() == false(4) add(1); add(2);

isEmpty() == false(5) add(1); add(2);

remove() == 1; isEmpty() == false;remove() == 2; isEmpty() == true

(6) add(1); add(2); add(3);remove() == 1; remove() == 2; remove() == 3

Dazu schreiben wir uns geeignete Funktionen. (Man könnte auch eine geeignete Klasse mit entsprechenden Methoden definieren, aber wir machen es uns an dieser Stelle einfach...) Diese sehen dann z. B. inklusive Header-Datei so aus:

1 /*** Beispiel queue1/tests.h ***/2 // führt eine Reihe von Queue-Tests durch und liefert true wenn alle erfolgreich waren;3 // andernfalls wird false zurückgegeben und auf der Standard-Ausgabe4 // eine entsprechende Fehlermeldung ausgegeben

127

Testfälle ableiten

gute Tests finden Fehler

Soll- und Ist-Wert

Queue-Tests

Page 136: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

5 bool performAllQueueTests ();1 /*** Beispiel queue1/tests.cpp ***/2 #include "tests.h"3 #include "queue.h" // die Queue-Klasse, die getestet werden soll4 #include <ostream>5 #include <iostream>6 #include <string>7 using namespace std;89 // Namensbereich für Funktionen, die Modul-intern sind

10 namespace11 {12 // Ausnahmeklasse, wird verwendet, um bei einem fehlerhaften Test das Testen abzubrechen13 class TestFailed {};1415 // Diese Funktion wertet "expression" aus:16 // Wenn der Ausdruck true ergibt, tut die Funktion nichts.17 // Wenn er false ergibt, wird eine Meldung unter Zuhilfenahme von „where“ (welcher Test?)18 // ausgegeben und eine Ausnahme vom Typ TestFailed ausgeworfen19 void check (bool expression, const string &where)20 {21 if (!expression)22 {23 cout24 << "Test fehlgeschlagen: " << where << endl;25 throw TestFailed ();26 }27 }28 // die verschiedenen Testfälle29 void test1 ()30 {31 Queue queue;32 check (queue.isEmpty () == true, "test1");33 }34 void test2 ()35 {36 Queue queue;37 queue.add (1);38 check (queue.isEmpty () == false, "test2");39 }40 void test3 ()41 {42 Queue queue;43 queue.add (1);44 check (queue.remove () == 1, "test3");45 check (queue.isEmpty () == true, "test3");46 }47 void test4 ()48 {49 Queue queue;50 queue.add (1);51 queue.add (2);52 check (queue.isEmpty () == false, "test4");53 }54 void test5 ()55 {56 Queue queue;57 queue.add (1);58 queue.add (2);59 check (queue.remove () == 1, "test5");60 check (queue.isEmpty () == false, "test5");

128

Page 137: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

61 check (queue.remove () == 2, "test5");62 check (queue.isEmpty () == true, "test5");63 }64 void test6 ()65 {66 Queue queue;67 queue.add (1);68 queue.add (2);69 queue.add (3);70 check (queue.remove () == 1, "test6");71 check (queue.remove () == 2, "test6");72 check (queue.remove () == 3, "test6");73 }74 } // Ende des Namensraumes für Modul-interne Funktionen7576 // siehe Deklaration im Header zur Beschreibung77 bool performAllQueueTests ()78 {79 // achte ab hier auf ausgeworfene Ausnahmen80 try81 {82 // führe alle Tests nacheinander durch83 test1 ();84 test2 ();85 test3 ();86 test4 ();87 test5 ();88 test6 ();89 // wenn wir hier angekommen sind, hat alles geklappt (keine Ausnahmen)90 return true;91 }92 catch (TestFailed) // fange Ausnahme-Objekte vom Typ TestFailed ab93 {94 // falls einer der Tests fehlgeschlagen hat, landen wir hier; gib false zurück95 return false;96 }97 }1 /*** Beispiel queue1/main.cpp ***/2 #include "tests.h"3 #include <ostream>4 #include <iostream>5 using namespace std;67 int main ()8 {9 if (performAllQueueTests ())10 {11 cout << "Queue: alle Tests bestanden." << endl;12 return 0; // alles OK13 }14 else15 {16 cout << "Queue: mindestens ein Test fehlgeschlagen!"17 << endl;18 return 1; // liefere einen Wert ungleich Null zurück, um Fehler anzuzeigen19 }20 }

Das ist eine Menge Programm-Quelltext, aber Testen erfordert eben auch Geduld und Gründlichkeit. Lassen Sie uns auf die Punkte eingehen, die für Sie neu sind:

129

Page 138: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

(1) Zeile 10 (tests.cpp): Hier lernen Sie ein neues Schlüsselwort kennen: namespace (dt. Namensraum). Ein Namensraum ist ein neuer Gültigkeitsbe­reich und wird verwendet, um Deklarationen logisch zu gruppieren und von an­deren Deklarationen abzugrenzen. Er ist häufig benannt. In unserem Beispiel ist er nicht benannt (oder anonym); dadurch sind alle Namen, die in diesem Na­mensraum deklariert werden, lokal zum Modul (d. h. zur Datei). Damit wird ver­mieden, dass es durch die Verletzung der Eine-Definition-Regel (3.3.1) zu Feh­lern kommt, etwa wenn beispielsweise in einem anderen Modul ebenfalls eine Funktion test1 definiert würde.

Ein anderes Beispiel zur Verwendung von Namensräumen ist die C++-Standard-Bibliothek. Sie haben bereits mehrfach in diesem Skript std::cout oder us­ing namespace std; erblickt. Nun, std ist ein Namensraum, in dem alle Deklarationen der C++-Standard-Bibliothek untergebracht sind. Dadurch werden Konflikte vermieden. Sie können beispielsweise eine selbstgeschriebene Funk­tion zum Zählen von Objekten count nennen, ohne sich darum zu kümmern, dass die C++-Standard-Bibliothek ebenfalls eine Funktion mit diesem Namen anbietet.

Mehr Informationen zu Namensräumen und zur C++-Standard-Bibliothek finden Sie in Kapitel 8.

Merksatz 17: Verwende Namensräume zur Modularisierung!

(2) Zeile 21-26 (tests.cpp): Innerhalb der check-Funktion wird der übergebe­ne Ausdruck expression auf seinen Wahrheitsgehalt hin überprüft. Wenn der Ausdruck logisch falsch ist, wird an dieser Stelle eine Ausnahme ausgeworfen (5.2). Dadurch kehrt die Funktion nicht normal zum Aufrufer zurück.

(3) Zeile 31 (tests.cpp): Hier wird eine Queue erzeugt. Wie Sie sehen, ist die Syntax nicht anders als bei der Definition einer Variable. Auch bei Objekten gilt, dass sie zerstört werden, wenn ihre Definition den Gültigkeitsbereich verlässt. Mehr zum Erzeugen und Zerstören von Objekten lernen Sie in den Abschnitten 4.5.1 und 4.5.2.

(4) Zeile 80-91 (tests.cpp): Dieser Abschnitt ist neu für Sie. Sie haben bereits erfahren, dass C++ bei Ausnahmen ein „außergewöhnliches“ Verlassen von Funktionen ermöglicht – über den throw-Ausdruck. Dabei wird ein Objekt ei­ner Klasse erzeugt und sozusagen „ausgeworfen“. Um die Ausnahme behandeln zu können, muss sie jedoch auch jemand „auffangen“. Dieser Block, der mit dem Schlüsselwort try eingeleitet ist, sorgt dafür, dass Ausnahmen innerhalb dieses Blocks überhaupt „beachtet“ werden. Das try (dt. versuchen) sagt so etwas aus wie „die Anweisungen in diesem Block könnten fehlschlagen; ich versuche mal, sie auszuführen“. Details finden Sie in den Abschnitten 5.2 und 5.3.

130

anonymer Na­mensraum

try-Block zum Beobachten von Ausnahmen

Page 139: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

(5) Zeile 92-96 (tests.cpp): Auf einen try-Block folgen immer ein oder mehre­re catch-Blöcke. In den catch-Blöcken werden die Ausnahmen aufgefangen (engl. to catch) und behandelt. Wir behandeln hier nur Ausnahme-Objekte vom Typ TestFailed. Das ist auch ausreichend, weil wir keine anderen Ausnah­men auswerfen. Die Programmlogik ist dabei folgende: Wenn ein Test fehl­schlägt, wird eine Ausnahme vom Typ TestFailed ausgeworfen, die dann in der Funktion performAllQueueTests aufgefangen wird. Dabei werden alle anderen Tests, die noch nicht ausgeführt worden sind, übersprungen. Es wird also nach dem ersten fehlschlagenden Test abgebrochen. Mehr zum Behandeln von Ausnahmen erfahren Sie in Abschnitt 5.3.

(6) Zeile 18 (main.cpp): Im Falle eines fehlgeschlagenen Tests wird nicht Null, wie bisher üblich, von der Funktion main zurückgegeben, sondern Eins. So be­kommt der Aufrufer des Programms die Möglichkeit, auf das Scheitern eines Tests geeignet zu reagieren.

Nun übersetzen Sie alle Dateien des Projekts, und führen Sie die Tests aus. Sie müss­ten eine Ausgabe erhalten, die Abbildung 33 ähnelt.

4.5 Objekte erzeugen, zerstören, und leben lassenDieser Abschnitt vermittelt Ihnen Kenntnisse über die Details der Objekt-Erzeugung und -Zerstörung. Sie lernen den wichtigen Begriff der Lebensdauer kennen und er­fahren, wie Objekte ihren eingeschränkten Gültigkeitsbereich „überdauern“ können.

4.5.1 Konstruktoren und InitialisierungAbschnitt 4.4.5.1 hat Sie darüber aufgeklärt, dass Sie innerhalb einer Klassen-Defini­tion keine Attribute initialisieren können. Darüber sind Sie natürlich unglücklich, weil Sie wissen, dass das Verwenden nicht initialisierter Variablen (und Attribute sind Variablen sehr ähnlich) ein grobes Vergehen ist, das mit langer und mühevoller Fehlersuche bestraft wird. Deshalb werden Sie jetzt darüber aufgeklärt, was ein Kon­struktor ist und wie er definiert und verwendet wird.

Ein Konstruktor wird während des Programmlaufs immer dann aufgerufen, wenn ein Objekt zu einer Klasse erzeugt wird. Er sorgt sich darum, dass alle Attribute zum Objekt erzeugt und initialisiert werden, und kann auch weitere vorbereitende Arbei­ten übernehmen, die nach der Erzeugung, aber vor der Benutzung eines Objekts er­forderlich sind.

Konstruktoren werden wie Methoden definiert, mit den folgenden Ausnahmen:

131

catch-Block zum Auffangen von Ausnahmen

Wie werden Attri­bute in C++ initi­alisiert?

Konstruktoren initialisieren Ob­jekte

Abbildung 33: Ausgabe der Queue-Tests

Page 140: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

(1) Konstruktoren heißen immer genauso wie die zugehörige Klasse.

(2) Ein Konstruktor hat keinen Rückgabetyp (nicht einmal void).

(3) Ein Konstruktor kann innerhalb einer Initialisierungsliste Attribute initialisieren.

Ein kleines Beispiel gefällig:1 /*** Beispiel ctor.cpp ***/2 #include <ostream>3 #include <iostream>4 using namespace std;56 // Diese Klasse repräsentiert einen einfachen Zähler.7 class Zaehler8 {9 public :

10 // Konstruktor. Initialisiert den Zähler mit übergebenem Startwert11 Zaehler (int startwert);12 // gibt den aktuellen Wert des Zählers zurück13 int gibWert () const;14 // erhöht den Zähler um Eins15 void erhoehe ();16 // addiert „delta“ zum Zähler17 void addiere (int delta);18 private :19 int wert;20 };2122 Zaehler::Zaehler (int startwert)23 :24 wert (startwert)25 {26 }2728 int Zaehler::gibWert () const29 {30 return wert;31 }3233 void Zaehler::erhoehe ()34 {35 addiere (1);36 }3738 void Zaehler::addiere (int delta)39 {40 wert += delta;41 }4243 int main ()44 {45 Zaehler zaehler (1);46 cout << "Wert des Zählers: " << zaehler.gibWert () << endl;47 zaehler.addiere (1);48 cout << "Wert des Zählers: " << zaehler.gibWert () << endl;49 zaehler.addiere (-2);50 cout << "Wert des Zählers: " << zaehler.gibWert () << endl;51 return 0;52 }

Die interessanten Konstrukte sind in:

132

Unterschiede zwi­schen Konstruk­toren und Metho­den

Page 141: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

• Zeile 11: Hier wird der Konstruktor innerhalb der Klasse deklariert. Beachten Sie, dass sein Name dem Klassennamen entspricht und er keinen Rückgabetyp besitzt.

• Zeile 22: Hier wird der Konstruktor definiert. Abgesehen vom fehlenden Rück­gabetyp (s. o.) unterscheidet er sich von einer normalen Methode wie erhoehe durch die Initialisierungsliste. Sie steht zwischen der schließenden runden Klam­mer, welche die Parameterliste abschließt, und der öffnenden geschweiften Klammer, welche den Anweisungsblock beginnt. Die Liste hat folgenden Auf­bau::

Attribut1 ( Ausdruck1 )[, Attribut2 ( Ausdruck2 )[, Attribut3 ( Ausdruck3 )[, ...]]]

Dabei wird jedes Attribut mit dem zugehörigen Ausdruck initialisiert.Die Initialisierungsliste ist optional, aber Sie wissen ja bereits: Sie sollten keine undefi­nierten Variablen und folglich keine undefinierten Attribute riskieren! Man kann es nicht oft genug wiederholen. Außerdem gibt es Momente, wo die Initialisierung der Ele­mente einer Klasse über diese Liste geschehen muss, nämlich wenn Konstanten oder Referenzen verwendet werden. Beispiel:

1 class Konstante2 {3 public :4 Konstante (int zuSetzenderWert);5 int gibWert () const;6 private :7 const int wert; // Konstante, da Schlüsselwort const8 };910 int Konstante::gibWert () const11 {12 return wert;13 }1415 Konstante::Konstante (int zuSetzenderWert)16 :17 wert (zuSetzenderWert) // initialisiere Konstante18 {19 wert = zuSetzenderWert; // das geht nicht!20 }

Die Zuweisung in Zeile 19 ist nicht erlaubt, weil Konstanten kein Wert zugewiesen wer­den darf. Initialisierung von Konstanten ist jedoch erlaubt und sogar erforderlich, des­halb ist Zeile 17 sowohl möglich als auch nötig.

Attribute werden nicht in der Reihenfolge der Initialisierungsausdrücke in der Initia­lisierungsliste eines Konstruktors ausgeführt. Vielmehr werden die Attribute in der Reihenfolge initialisiert, in der sie in der Klassen-Definition stehen. Im folgenden Beispiel wird also das Attribut m_x vor dem Attribut m_y initialisiert, obwohl der

133

Initialisierungs­liste im Konstruk­tor

Konstanten müs­sen im Konstruk­tor initialisiert werden

Reihenfolge der Initialisierung von Attributen

Page 142: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

Initialisierungsausdruck für m_x in der Initialisierungsliste im Konstruktor nach je­nem für m_y notiert ist:

1 class Point2 {3 private :4 int m_x;5 int m_y;6 public :7 Point (int x, int y);8 };9 Point::Point (int x, int y)

10 :11 m_y (y),12 m_x (x)13 {14 }

Unterschiedliche Reihenfolgen von Attributen in der Klassen-Definition und in einer Konstruktor-Initialisierungsliste sollten unbedingt vermieden werden, da ansonsten böse Fehler auftauchen können (siehe nächsten Abschnitt):

Merksatz 18: Verwende eine einheitliche Reihenfolge bei Attributen!

Eine unterschiedliche Reihenfolge der Attribute in der Klassen-Definition und in der In­itialisierungsliste kann verwirren und schlimmstenfalls Programmfehler verursachen. Letzteres kann passieren, wenn Initialisierungsausdrücke von zuvor initialisierten Attri­buten abhängen. Das obige Beispiel, leicht umformuliert, zeigt uns die Probleme, die dabei auftauchen können:

1 class Point2 {3 private :4 int m_x;5 int m_y;6 public :7 Point (int x, int y);8 };9 Point::Point (int x, int y)

10 :11 m_y (y),12 m_x (x + m_y)13 {14 }

Hier soll in Zeile 12 das Attribut m_x auf die Summe des Parameters x und des Attri­buts m_y gesetzt werden. Doch ist dabei nicht bedacht worden, dass das Attribut m_y zu diesem Zeitpunkt noch keinen wohldefinierten Wert beinhaltet, da dessen Initialisie­rung noch nicht erfolgt ist – ungeachtet der Tatsache, dass der Initialisierungsausdruck für m_y lexikalisch vor dem Initialisierungsausdruck für m_x steht. Der obige Quelltext führt also zu undefiniertem Verhalten, was im schlimmsten Fall einen Programmabsturz zur Folge haben kann.

Wenn Sie in Java programmieren, dann müssen Sie besonders auf die Initialisierung von Konstanten achten, da Java nicht zwischen Zuweisung und Initialisierung unter­scheidet. Stattdessen ist es erlaubt und notwendig, während der Konstruktion eines Ob­jekts final-Attributen einen Wert zuzuweisen.

• Zeile 25/26: Zwischen den beiden geschweiften Klammern können beliebige Anweisungen stehen, die zur Initialisierung des Objekts notwendig sind. In die­

134

Aufpassen bei Ab­hängigkeiten zwi­schen Attributen!

Page 143: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

sem Fall haben wir keine benötigt, aber es gibt durchaus Fälle, in denen die Initi­alisierung eines Objekts komplexer ist als lediglich die Belegung von Attributen mit Werten.

• Zeile 45: Hier wird ein Objekt der Klasse Zaehler erzeugt. Sie sehen, dass die funktionale Schreibweise bei der Initialisierung verwendet wurde (3.3.3). Dieser Initialwert wird bei der Konstruktion des Objekts an den Konstruktor übergeben. Der Begriff der funktionalen Initialisierung wird jetzt deutlich: Der Konstruktor wird wie eine Funktion aufgerufen und eventuelle Ausdrücke zwischen den Klammern als Argumente übergeben.

Jetzt verstehen Sie auch, warum bei Objekten die funktionale Schreibweise bei der Initialisierung mehr Sinn macht: Wenn ein Konstruktor mehr als einen Para­meter erfordert, kommen Sie nicht um diese Schreibweise umhin, denn in der an­deren Schreibweise (die mit dem Gleichheitszeichen) können Sie nur einen Aus­druck angeben, und der Übersetzer wird meckern, dass Argumente fehlen.

Das Programm gibt übrigens aus:Wert des Zählers: 1Wert des Zählers: 2Wert des Zählers: 0

Jetzt wo Sie die grundlegende Syntax für die Definition und Verwendung von Kon­struktoren kennen gelernt haben, ist es an der Zeit, mehr über die Eigenarten von Konstruktoren zu erfahren. Zunächst ist sicherlich die Reihenfolge bei der Initialisie­rung interessant. Dass die Attribute in der Reihenfolge der Definition innerhalb der Klasse initialisiert werden, wissen Sie ja bereits. Wichtig ist in diesem Zusammen­hang, dass diese Initialisierungen vor dem Ausführen des Anweisungsblocks des Konstruktors durchgeführt werden. Dadurch wird sichergestellt, dass Anweisungen im Konstruktor auf die Attribute der Klasse problemlos zugreifen können und nicht Gefahr laufen, nicht initialisierte Objekte zu benutzen. Zusammengefasst sieht die Reihenfolge bei der Initialisierung also so aus:

(1) Zuerst werden die Konstruktoren der Basisklassen (soweit vorhanden) ausge­führt (siehe in diesem Zusammenhang die Abschnitte 4.6.4 und 4.6.5).

(2) Danach werden alle Attribute in der Reihenfolge der Definitionen innerhalb der Klasse initialisiert. Handelt es sich dabei um objektwertige Attribute, werden entsprechende Konstruktoren aufgerufen.

(3) Schließlich wird der Anweisungsblock des Konstruktors ausgeführt.

Weiterhin ist es wichtig zu wissen, was mit Klassen geschieht, in denen Sie keinen Konstruktor definieren. Es gilt folgende Regel: Immer wenn die explizite Definition eines Konstruktors fehlt, wird eine implizite Definition eines Konstruktors ohne Pa­rameter und ohne Anweisungen generiert, der sogenannte Default-Konstruktor. Alle Klassen, die Sie bis zu diesem Abschnitt kennen gelernt haben, etwa die Klasse Queue, hatten einen solchen Default-Konstruktor. Ein Default-Konstruktor sieht im­mer folgendermaßen aus:

Klassenname :: Klassenname ( )

135

jede Klasse hat einen Konstruk­tor, den Default-Konstruktor

Reihenfolge der Initialisierung

Page 144: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

{}

Offensichtlich führt dieser generierte Konstruktor keine explizite Initialisierung der Attribute einer Klasse durch, deshalb ist die Verwendung eines Default-Konstruktors immer dann nicht erlaubt, wenn der obige Konstruktor nicht erlaubt ist, beispielswei­se wenn die Klasse konstante Attribute oder Referenzen enthält (siehe Klasse Kon­stante weiter oben).

Wenn Sie ein Attribut nicht in der Initialisierungsliste eines Konstruktors initialisie­ren, hat es nur dann einen undefinierten Inhalt, wenn es nicht vom Typ einer Klasse ist, d. h. bei primitiven Datentypen (etwa int oder float) sowie bei zusammenge­setzten Datentypen (etwa char *). Ist das Attribut ein Objekt (also vom Typ einer Klasse), wird für das Objekt bei fehlender Initialisierung der Default-Konstruktor aufgerufen. Somit wird sichergestellt, dass Objekte immer korrekt initialisiert sind. Ist kein solcher Default-Konstruktor vorhanden oder nicht nutzbar (etwa weil er private ist), ist das C++-Programm fehlerhaft. Beispiel:

1 // die Klasse Konstante sei wie im obigen Beispiel definiert2 class LeereKlasse3 {4 // jede Klasse ohne explizite Konstruktoren hat einen impliziten5 // Default-Konstuktor, s. u.6 };78 class Container9 {

10 public :11 // expliziter Default-Konstruktor12 Container ();13 private :14 Konstante k;15 LeereKlasse l;16 };1718 Container::Container ()19 :20 k (1) // explizite Initialisierung notwendig21 // Initialisierung von "l" entfällt, da dies der Übersetzer implizit erledigt22 {23 }24

In diesem Beispiel enthält die Klasse Container zwei Attribute, eines vom Typ Konstante und eines vom Typ LeereKlasse. Die Klasse LeereKlasse hat einen Default-Konstruktor, der zur Initialisierung benutzt werden soll, somit kann im Container-Konstruktor die explizite Initialisierung des Attributs l entfallen. Die Klasse Konstante besitzt hingegen keinen Default-Konstruktor (weil ein benutzer­definierter Konstruktor existiert), somit muss die Initialisierung des Attributs k expli­zit angegeben werden.

136

automatische Nutzung von Default-Kon­struktoren

Page 145: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

4.5.2 Destruktoren und RAIIEin Destruktor ist der Pendant zum Konstruktor. Der Destruktor wird aufgerufen, wenn ein Objekt zerstört wird. Dabei macht der Destruktor im Regelfall die im Kon­struktor vorgenommenen Operationen rückgängig. (Der Name kommt übrigens von destruieren = zerstören.)

Merksatz 19: Betrachte Konstruktoren und Destruktor als Team!

Ein kleines Beispiel zum Verständnis: Stellen Sie sich vor, Sie haben eine Klasse File, welche eine Abstraktion einer Datei darstellt und Operationen zum Öffnen, Lesen, Schreiben und Schließen von Dateien enthält:

1 /*** Beispiel file1.cpp ***/2 #include <ostream>3 #include <iostream>4 #include <string>5 using namespace std;67 // Repräsentiert eine Datei.8 class File9 {10 public :11 // öffnet die angegebene Datei; liefert true bei Erfolg und false bei Misserfolg zurück12 bool open (string name);1314 // schließt die zuvor geöffnete Datei15 void close ();1617 // liest „count“ Zeichen aus der zuvor geöffneten Datei18 string read (int count);1920 // schreibt die Zeichenkette „data“ in die zuvor geöffnete Datei21 void write (string data);2223 private :24 // von der Implementierung benötigte Attribute25 };

Ohne weiter die Implementierung kennen zu lernen, lassen Sie uns betrachten, wie diese Klasse zu benutzen ist. Der Klient muss zunächst ein File-Objekt erzeugen. Dann kann er diesem die Nachricht open, versehen mit einem Dateinamen, schicken und hoffen, dass diese Nachricht true zurückliefert, was auf ein erfolgreiches Öff­nen hindeutet. Das Öffnen beansprucht gewöhnlich Betriebssystem-Ressourcen. Da­nach kann er die read- und write-Nachrichten nach Belieben aufrufen. Wenn der Klient fertig ist, ruft er close auf, um die Datei wieder zu schließen und die beim Öffnen angeforderten Betriebssystem-Ressourcen freizugeben. Beispiel:

26 /*** Beispiel file1.cpp (Fortsetzung) ***/27 int main ()28 {29 File myFile;30 if (myFile.open ("myfile.dat"))31 {32 string vier = myFile.read (4);33 cout << "die ersten 4 Zeichen: " << vier << endl;34 myFile.close ();

137

Destruktoren zer­stören Objekte

Destruktoren am Beispiel

Page 146: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

35 }36 else37 {38 cout << "Konnte Datei nicht öffnen!" << endl;39 }40 return 0;41 }

Haben Sie die Gefahren bemerkt, die sich beim Benutzen dieser Klasse einschleichen können? Mindestens zwei Dinge sollten zum Nachdenken anregen:

(1) Es ist möglich, die Operation read aufzurufen, ohne vorher eine Datei über open geöffnet zu haben. Da man aus einer nicht geöffneten Datei nicht lesen kann, wird dies sicherlich einen Fehler produzieren.

(2) Es ist möglich, den Aufruf der close-Operation zu vergessen und somit ange­forderte System-Ressourcen nicht ordnungsgemäß freizugeben. Man spricht in einem solchen Fall von einem Ressourcen-Leck.

Beides ist ungünstig, kann aber mit den Sprachmitteln von C++ leicht behoben wer­den:

(1) Zum ersten Problem: Wir beheben diese Gefahr, indem wir festlegen, dass ein File-Objekt immer eine geöffnete Datei repräsentiert. Bisher konnte ein File-Objekt zwei Zustände haben: geöffnet und nicht geöffnet, was zum oben be­schriebenen Problem führen kann. Wenn wir aber sichergehen wollen, dass ein File-Objekt immer für eine geöffnete Datei steht, müssen wir die open-Opera­tion verpflichtend machen. Das geht nur, wenn wir bereits im Konstruktor die Datei öffnen, denn den Konstruktor muss der Klient benutzen – denn sonst hat er überhaupt kein Objekt, mit dem er etwas anfangen kann

Das bedeutet, dass wir die Schnittstelle der Klasse entsprechend abändern müs­sen. Zum ersten muss der Konstruktor bereits den Dateinamen bekommen, damit er die Datei öffnen kann. Zum zweiten darf die open-Operation nicht mehr öf­fentlich sein, denn es macht keinen Sinn, dem Klienten das Öffnen einer bereits geöffneten Datei zu erlauben. Zum dritten muss der Konstruktor die Möglichkeit haben, einen Fehler beim Öffnen der Datei dem Aufrufer mitzuteilen. Bisher hat die open-Operation einfach false zurückgeliefert. Da ein Konstruktor keinen Rückgabewert hat, bleibt ihm nichts anderes übrig, als im Fehlerfall eine geeig­nete Ausnahme auszuwerfen. (Mehr über Ausnahmen erfahren Sie in Kapitel 5.)

Das resultiert in den folgenden Klassen und Definitionen:1 /*** Beispiel file2.cpp (vorläufig) ***/2 #include <ostream>3 #include <iostream>4 #include <string>5 using namespace std;67 // wird ausgeworfen, wenn das Öffnen der Datei fehlschlägt8 class FileOpenException9 {

10 };1112 class File

138

potentielle Gefah­ren

Ressourcen-Lecks

Konstruktoren akquirieren Res­sourcen

Konstruktoren ge­nerieren bei Feh­lern Ausnahmen

Page 147: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

13 {14 public :15 // Konstruktor: öffnet die angegebene Datei16 // wirft eine FileOpenException-Ausnahme aus, wenn das Öffnen fehlschlägt17 File (string name);1819 // schließt die zuvor geöffnete Datei20 void close ();2122 // liest „count“ Zeichen aus der zuvor geöffneten Datei23 string read (int count);2425 // schreibt die Zeichenkette „data“ in die zuvor geöffnete Datei26 void write (string data);2728 private :29 // von der Implementierung benötigte Attribute3031 // öffnet die angegebene Datei; liefert true bei Erfolg und false bei Misserfolg zurück32 bool open (string name);33 };3435 File::File (string name)36 {37 // öffne Datei; wenn das fehlschlägt, wirf eine Ausnahme aus38 if (!open (name))39 throw FileOpenException ();40 }

(2) Zum zweiten Problem: Wir wollen sicherstellen, dass alle Ressourcen ordnungs­gemäß an das System zurückgegeben werden. Wir wollen uns nicht darauf ver­lassen, dass der Klient eine entsprechende Operation aufruft, denn er könnte dies vergessen. C++ garantiert uns jedoch, dass vor der Zerstörung eines Objekts sein Destruktor aufgerufen wird. Also verlegen wir das Schließen der Datei in den Destruktor. Analog zur obigen Änderung werden wir auch die close-Operation privat machen, um zu verhindern, dass ein Klient erst eine Datei schließt und da­nach versucht, daraus zu lesen:

1 /*** Beispiel file2.cpp (Endfassung) ***/2 #include <ostream>3 #include <iostream>4 #include <string>5 using namespace std;67 // wird ausgeworfen, wenn das Öffnen der Datei fehlschlägt8 class FileOpenException9 {10 };1112 class File13 {14 public :15 // Konstruktor: öffnet die angegebene Datei16 // wirft eine FileOpenException-Ausnahme aus, wenn das Öffnen fehlschlägt17 File (string name);1819 // Destruktor, schließt die geöffnete Datei20 ~File ();21

139

Destruktoren ge­ben Ressourcen frei

Page 148: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

22 // liest „count“ Zeichen aus der zuvor geöffneten Datei23 string read (int count);2425 // schreibt die Zeichenkette „data“ in die zuvor geöffnete Datei26 void write (string data);2728 private :29 // von der Implementierung benötigte Attribute3031 // öffnet die angegebene Datei; liefert true bei Erfolg und false bei Misserfolg zurück32 bool open (string name);3334 // schließt die zuvor geöffnete Datei35 void close ();36 };3738 File::File (string name)39 {40 // öffne Datei; wenn das fehlschlägt, wirf eine Ausnahme aus41 if (!open (name))42 throw FileOpenException ();43 }4445 File::~File ()46 {47 // schließe die Datei und gib Ressourcen frei48 close ();49 }

Wie Sie sehen, werden Destruktoren wie Konstruktoren definiert, nur haben sie zusätzlich eine Tilde (~) vor Ihrem Namen und generell keine Parameter. (Es gäbe ja keine Möglichkeit, einem Destruktor jemals Argumente zu übergeben, weil er implizit von der Laufzeit-Umgebung beim Zerstören eines Objekts aufge­rufen wird.)

Da diese Änderungen die öffentliche Schnittstelle verändert haben, müssen wir auch die main-Funktion entsprechend anpassen:

50 /*** Beispiel file2.cpp (Fortsetzung) ***/51 int main ()52 {53 try54 {55 File myFile ("myfile.dat");56 string vier = myFile.read (4);57 cout << "die ersten 4 Zeichen: " << vier << endl;58 // hier endet der Gültigkeitsbereich von myFile; dabei wird automatisch der59 // Destruktor aufgerufen und die geöffnete Datei geschlossen60 }61 catch (FileOpenException)62 {63 // wenn wir hier ankommen, hat das Öffnen der Datei nicht geklappt64 cout << "Konnte Datei nicht öffnen!" << endl;65 }66 return 0;67 }

140

Form des De­struktors

Page 149: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

Wie Sie sehen, ist die main-Funktion sogar um eine Zeile kürzer geworden (wenn Sie sich die Kommentare wegdenken), da Erstellung des File-Objekts und Öffnen der Datei jetzt in einer Operation zusammengefasst sind.

Konstruktor und Destruktor geben also ein gutes Team ab beim Verwalten von Res­sourcen, die bei einem Objekt bei der Initialisierung angefordert und bei der Zerstö­rung freigegeben werden sollen. Das beschriebene Vorgehen hat in der C++-Literatur auch einen Namen: Resource Acquisition is Initialization (RAII), zu deutsch Res­sourcenbelegung ist Initialisierung. Dieses wichtige und häufig genutzte C++-Idiom stellt auf einfache Weise die korrekte Freigabe von Ressourcen sicher.

In Java gibt es, Destruktoren ähnlich, finalize-Methoden, doch sind sie nicht annä­hernd so nützlich wie in C++, weil in Java nicht vorhergesagt werden kann, wann die finalize-Methode aufgerufen wird. Da der Zeitpunkt aber oft wichtig ist, z. B. wenn im Destruktor eine Sperre freigegeben wird, tendieren Java-Programmierer dazu, die Ressourcen in solchen Fällen explizit freizugeben, mit allen beschriebenen Nachteilen.

Ein weiterer, unglaublich praktischer Vorteil von Destruktoren ist die Tatsache, dass die Destruktoren auch dann ausgeführt werden, wenn das Objekt durch eine Ausnah­me bedingt zerstört wird. Genauer gehen wir darauf in Abschnitt 5.9 ein.

In Java haben Sie nur die Möglichkeit, try-finally-Konstruktionen zu verwenden, wenn Sie aufräumenden Code bei einer Ausnahme ausführen wollen. try-Konstrukte tendieren jedoch generell dazu, lang und unübersichtlich zu werden, sowie sich zu wie­derholen. Die Destruktoren von C++ bieten hingegen eine übersichtliche und kurze Al­ternative.

Destruktoren haben – wie Konstruktoren – auch noch eine andere Aufgabe: Sie zer­stören nicht nur das Objekt, für das sie aufgerufen werden, sondern auch alle „abhän­gigen“ Objekte, also alle Attribute (und Basisklassen, s. Abschnitte 4.6.4 und 4.6.5). Dabei geschieht das Zerstören in der umgekehrten Reihenfolge der Konstruktion:

(1) Zuerst wird der Anweisungsblock im Destruktor ausgeführt.

(2) Dann wird für alle Attribute der Destruktor aufgerufen, und zwar in umgekehrter Initialisierungsreihenfolge; d. h. das Attribut, das innerhalb der Klassendefinition als letztes definiert wird, wird als erstes zerstört.

(3) Schließlich wird der Destruktor der Basisklasse(n) aufgerufen, ebenfalls in um­gekehrter Initialisierungsreihenfolge.

Diese Reihenfolge macht Sinn: Es wird sichergestellt, dass nicht Teile der Klasse nach ihrer Zerstörung verwendet werden können. Das könnte bei einer anderen Rei­henfolge passieren, etwa wenn die Attribute vor der Ausführung des Anweisungs­blocks des Destruktors zerstört würden und der Anweisungsblock diese noch benut­zte.

Ebenso wie es einen Default-Konstruktor für Klassen gibt, die keinen eigenen Kon­struktor definieren, wird ein Default-Destruktor definiert, wenn eine Klasse keinen explizit angibt. Dieser Default-Destruktor sieht so aus:

Klassenname :: ~ Klassenname ( ){

141

Resource Acqui­sition is Initial­ization (RAII)

Ressourcen-Frei­gabe auch bei Ausnahmen

Default-Destruk­tor

Reihenfolge bei der Zerstörung

Page 150: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

}und hat somit keinerlei Seiteneffekte (außer der – in jedem Destruktor immer durch­geführten – Zerstörung aller Attribute und Basisklassen).

Merksatz 20: Verwende Destruktoren zur Ressourcen-Freigabe!

4.5.3 Kopien und die Sache mit der LebensdauerJetzt kommen wir zu einem interessanten Thema: der Lebensdauer von Objekten. Bisher war die Lebensdauer immer recht einfach zu ermitteln: Ein Objekt war so lan­ge existent, wie das Programm innerhalb seines Gültigkeitsbereiches agierte. Verließ der Kontrollfluss den jeweiligen Block mit der Objekt-Definition, so wurde das Ob­jekt zerstört.

Fassen wir kurz die bisherigen Regeln zur Lebensdauer und Gültigkeitsbereich zu­sammen (mit Objekten sind nachfolgend Variablen und Konstanten zu allen mögli­chen Typen gemeint, nicht nur zu Klassen):

• Objekte, die innerhalb einer Funktion oder einer Methode definiert werden, sind bis zum Ende ihres Blocks existent. Ihr Gültigkeitsbereich erstreckt sich von dem Ort der Definition bis zum Ende des enthaltenden Blocks.

• Objekte, die innerhalb von Klassen definiert werden (Attribute), sind an das ent­haltende Objekt gebunden. Deren Lebensdauer ist genauso lang wie die des ent­haltenden Objekts. Der Gültigkeitsbereich erstreckt sich über alle Konstruktoren, Destruktoren und Methoden der Klasse.

Bisher war es also so, dass ein Objekt nie länger „leben“ konnte als in dem Zeitraum von seiner Definition im Quelltext bis zum Ende des umfassenden Blockes. Doch was, wenn das Objekt länger „leben“ muss als der definierende Block? Was ist bei­spielsweise, wenn eine Methode oder Funktion ein Objekt zurückgeben möchte, bei­spielsweise ein Queue-Objekt?

1 // liefert die ersten vier Primzahlen in einer Queue zurück2 Queue dieErstenVierPrimzahlen ()3 {4 Queue queue;5 queue.add (2);6 queue.add (3);7 queue.add (5)8 queue.add (7)9 return queue;

10 }

Wie kann die Objekt-Variable queue zurückgeliefert werden, wenn sie doch am Ende des Funktions-Blocks zerstört wird?

Die Antwort ist: Das Objekt queue wird gar nicht zurückgegeben, sondern eine Ko­pie. Das ursprüngliche Objekt wird sozusagen ordnungsgemäß zerstört und „lebt“ in einer Kopie „weiter“. Ein Aufruf der obigen Funktion könnte beispielsweise so aus­sehen:

11 Queue primzahlen = dieErstenVierPrimzahlen ();

142

Lebensdauer von Objekten

bisher Kopplung von Lebensdauer und Gültigkeits­bereich

Kopien werden erstellt

Page 151: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

Oder so:11 Queue primzahlen (dieErstenVierPrimzahlen ());

Ähnliches geschieht, wenn Sie Objekte in einen Container der C++-Standard-Biblio­thek speichern. Auch diese Container speichern Kopien.

Doch was genau ist eine Kopie? Wie wird sie erzeugt? Und was ist, wenn keine Ko­pie gewünscht oder möglich ist?

4.5.3.1 Der Kopier-KonstruktorEine Kopie eines Objekts ist erst einmal genauso ein „gewöhnliches“ Objekt wie das Original. Es gibt keinen Unterschied zwischen einem Original und einer Kopie:

(1) Der Typ ist derselbe.

(2) Der Inhalt ist der gleiche: Alle Attribute des Original-Objekts werden mitkopiert und sind somit Teil der Kopie.

Der letzte Punkt deutet schon an, wie eine Kopie angefertigt wird, nämlich rekursiv. Zuerst wird die „Hülle“ der Kopie erzeugt und dann in jedes Attribut in dieser Hülle das entsprechende Attribut aus dem Original kopiert. Ist ein Attribut selbst ein Ob­jekt, müssen dessen Attribute auf die gleiche Art und Weise kopiert werden, wobei diese wieder objektwertig sein können u. s. w.

Sie wissen aber, dass ein Objekt in C++ immer durch einen Konstruktor initialisiert wird. Der Default-Konstruktor (4.5.1) scheidet jedoch aus, denn dieser initialisiert das Objekt nicht oder zumindest nicht so wie das Original. Da Sie jedoch in C++ auch Objekte kopieren können, deren Klassen überhaupt keine Konstruktoren defi­nieren, muss es noch einen zweiten Konstruktor geben, der standardmäßig definiert ist und sich um das Erzeugen von Kopien kümmert. Diesen Konstruktor gibt es wirk­lich: Es ist der sogenannte Kopier-Konstruktor.

Der Kopier-Konstruktor hat die folgende Deklaration:

Klassenname ( const Klassenname & Parametername );Der Kopier-Konstruktor bekommt als Argument das Objekt übergeben, das bei der Kopier-Operation als Quell-Objekt fungiert. Dies ist u. a.

• der Initialisierungsausdruck beim direkten Initialisieren (3.3.3) einer Kopie,

• das zugehörige Argument bei der Initialisierung eines Parameters (3.6.4),

• der Rückgabewert einer return-Anweisung (3.5.5) bei der Rückkehr von einer Funktion,

• das Ausnahme-Objekt beim Betreten eines catch-Blocks (5.3).

Falls der Kopier-Konstruktor nicht explizit für eine Klasse definiert wird, erstellt der C++-Übersetzer eine Standard-Implementierung, die alle Attribute eines Objekts ko­piert. Das Kopieren der Attribute wird entweder wieder über den entsprechenden Ko­pier-Konstruktor erledigt (falls es sich um objektwertige Attribute handelt) oder aber

143

Was ist eine Ko­pie?

rekursive Erstel­lung von Kopien

Kopier-Konstruk­tor

Form des Kopier-Konstruktors

Wann werden Objekte kopiert?

Standard-Imple­mentierung des Konstruktors

Page 152: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

als (bitweise) Zuweisung (in allen anderen Fällen, etwa bei Attributen vom Typ int).

Natürlich kann der Kopier-Konstruktor auch mit einer eigenen Definition belegt wer­den. Das ist oft unerlässlich, wenn die Kopie eines Objekts eben nicht eins-zu- eins erfolgen darf. Bei unserer File-Klasse (4.5.1) zum Beispiel ist es fraglich, ob eine Eins-zu-eins-Kopie vernünftig wäre. Überlegen Sie einmal: Der Destruktor der File-Klasse gibt die angeforderten System-Ressourcen, die zur geöffneten Datei gehören, durch den Aufruf von close frei. Wenn nun eine Kopie eines solchen File-Objekts erstellt wird, werden spätestens am Ende des Programms zwei De­struktoren und somit zwei close-Methoden aufgerufen, was zur doppelten Freigabe derselben System-Ressource führt. Die meisten Betriebssysteme mögen dies über­haupt nicht. Also kann eine Eins-zu-eins-Kopie hier nicht richtig sein. Es gibt mehre­re Lösungen in diesem speziellen Fall, nach aufsteigender Schwierigkeit geordnet:

(1) Das Kopieren von File-Objekten wird generell verboten (s. u.)

(2) Beim Kopieren wird die Ressource über einen geeigneten System-Aufruf dupli­ziert, so dass aus der Sicht des Betriebssystems zwei Ressourcen für dieselbe Da­tei existieren.

(3) Es wird ein Zähl-Mechanismus eingeführt, so dass eine Ressource erst freigege­ben wird, wenn kein File-Objekt mehr auf sie verweist (Stichwort „Referenz­zähler“).

Unabhängig von der Wahl der Methode haben Sie hoffentlich gemerkt, dass der Pro­gramm-Entwickler sich über das Kopieren von Objekten Gedanken machen muss, da es in C++ fest eingebaut, aber nicht immer passend ist.

Verdeutlichen wir die gewonnenen Kenntnisse, indem wir das Beispiel aus dem Ab­schnitt über const-Operationen (4.4.7.1) leicht abändern:

1 /*** Beispiel copyctor.cpp ***/2 #include <ostream>3 #include <iostream>4 using namespace std;56 // Diese Klasse kapselt einen Wert.7 class Wert8 {9 public :

10 // Konstruktor initialisiert das Objekt und setzt den Startwert (s. 4.5.1)11 Wert (int startwert);1213 // Kopier-Konstruktor: kopiert den Wert und vermerkt, dass es sich um eine Kopie handelt14 Wert (const Wert &quelle);1516 // gibt den gespeicherten Wert zurück17 int gibWert () const;1819 // liefert true zurück falls das Objekt keine Kopie ist20 bool istOriginal () const;2122 // ändert den Wert23 void setzeNeuenWert (int neuerWert);

144

eigene Definition eines Kopier-Konstruktors

Kopieren erlau­ben? Wenn ja, wie?

Page 153: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

2425 private :26 int wert;27 bool original;28 };2930 Wert::Wert (int startwert)31 :32 wert (startwert),33 original (true)34 {35 }3637 Wert::Wert (const Wert &quelle)38 :39 wert (quelle.gibWert ()),40 original (false)41 {42 }4344 int Wert::gibWert () const45 {46 return wert;47 }4849 bool Wert::istOriginal () const50 {51 return original;52 }5354 void Wert::setzeNeuenWert (int neuerWert)55 {56 wert = neuerWert;57 }5859 int main ()60 {61 // ein „originales“ Objekt62 const Wert o1 (42);63 // eine Kopie davon64 const Wert o2 = o1;65 // eine Kopie von der Kopie66 const Wert o3 (o2);6768 // Ausgabe, ob die Objekte Originale oder Kopien sind69 cout << "o1 ist " <<70 (o1.istOriginal () ? "Original" : "Kopie") << endl;71 cout << "o2 ist " <<72 (o2.istOriginal () ? "Original" : "Kopie") << endl;73 cout << "o3 ist " <<74 (o3.istOriginal () ? "Original" : "Kopie") << endl;7576 return 0;77 }

Folgende Anmerkungen zum Programm:

• Zeile 14: Ein expliziter Kopier-Konstruktor ist hinzugekommen.

• Zeile 27: Das Attribut original gibt an, ob ein Objekt mit Hilfe des normalen Konstruktors oder mit Hilfe des Kopier-Konstruktors erzeugt wurde. Dadurch

145

Page 154: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

kann man erkennen, ob ein Objekt eine Kopie ist (original == false) oder nicht.

• Zeile 33: Der normale Konstruktor setzt das Attribut original auf true, um anzuzeigen, dass das Objekt keine Kopie ist.

• Zeilen 37-42: Hier ist die Definition des Kopier-Konstruktors. Der Kopier-Kon­struktor muss genauso wie der normale Konstruktor jedes Attribut initialisieren. Das Attribut wert initialisiert er mit dem entsprechenden Wert aus dem Origi­nal, das Attribut original hingegen setzt er immer auf false, um festzuhal­ten, dass dieses Objekt eine Kopie ist.

• Zeilen 61-66: Hier werden drei Objekte erzeugt, eines mit Hilfe des normalen Konstruktors und zwei als Kopie. Dabei wird noch einmal verdeutlicht, dass wenn der zugehörige Konstruktor genau einen Parameter hat (etwa der Kopier-Konstruktor), sowohl die funktionale Schreibweise der Initialisierung funktio­niert (Zeile 66) als auch diejenige mit dem Gleichheitszeichen (Zeile 64). Bei Objekten und Konstruktoren ist jedoch die funktionale Schreibweise vorzuzie­hen.39

Das Programm erzeugt folgende Ausgabe:o1 ist Originalo2 ist Kopieo3 ist Kopie

4.5.3.2 Verhindern von KopienEs gibt Situationen, in denen für eine Klasse von Objekten das Kopieren keinen Sinn macht. Beispielsweise gibt es Klassen, von denen es zur Programm-Laufzeit genau ein Objekt gibt (Singleton, s. Abschnitt 6.4.2) – nicht mehr aber auch nicht weniger. Das Kopieren eines solchen Objekts muss natürlich verboten werden.

In C++ geht dies sehr einfach. Wissen Sie bereits, wie? Die notwendigen Sprachmit­tel haben Sie nämlich bereits kennen gelernt...

Wenn Sie nicht darauf kommen, hier die Lösung: Sie deklarieren den Kopier-Kon­struktor private. Dadurch wird allen verboten, Objekte dieser Klasse zu kopieren. Einfach, nicht? Aber schließlich ist ein Kopier-Konstruktor in gewisser Weise eine Operation wie jede andere und fällt somit auch unter die Regeln der Zugriffs-Modifi­zierer.

Das bedeutet aber, dass Objekte der eigenen Klasse sich weiter kopieren dürfen, weil der Zugriffsmodifizierer private nur allen anderen verbietet, Objekte zu kopieren. Deshalb sollten Sie aufpassen, dass Sie selbst in Ihrer eigenen Klasse nicht aus Verse­hen Ihre Objekte kopieren.

39) Es können jedoch unter gewissen Umständen Mehrdeutigkeiten auftreten, wenn Sie ein Objekt mit nur einem Ausdruck und der funktionalen Schreibweise initialisieren. In solchen Fällen wird der Übersetzer nicht ein Objekt definieren, sondern eine Funktion (!) deklarieren. Da es ziemlich schwierig ist, die genauen Umstände mit einfachen Worten zu erklären, verwenden Sie einfach die Schreibweise mit dem Gleichheitszeichen, falls der Übersetzer bei einer Initialisierung meckert.

146

Kopien nicht im­mer sinnvoll

privater Kopier-Konstruktor

Page 155: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

Den so deklarierten Kopier-Konstruktor müssen Sie übrigens nicht definieren, da Sie ihn nicht benutzen werden. Das ist generell in C++ so: Objekte, Typen oder Funktio­nen, die deklariert aber nicht benutzt werden, brauchen keine Definition. Dies löst auch das o. g. Problem: Wenn Sie fälschlicherweise Objekte Ihrer eigenen Klasse ko­pieren, obwohl dies eigentlich verboten sein soll, wird spätestens der Binder me­ckern, weil er die Definition des Kopier-Konstruktors nicht finden wird.

Beispiel:1 // Objekte dieser Klasse lassen sich nicht kopieren!2 class DoNotCopy3 {4 public :5 // normaler Konstruktor6 DoNotCopy ();7 private :8 // Kopier-Konstruktor (braucht nicht definiert zu werden)9 DoNotCopy (const DoNotCopy &quelle);10 };

Was ist aber, wenn Objekte wie eingangs erwähnt „lange“ leben müssen und bei­spielsweise innerhalb einer Funktion erzeugt und zurückgegeben werden? Das fol­gende Beispiel funktioniert ja nicht, wenn das Objekt keinen Kopier-Konstruktor hat:

1 // gibt ein DoNotCopy-Objekt zurück2 DoNotCopy erzeugeDoNotCopyObjekt ()3 {4 DoNotCopy objekt;5 return objekt; // funktioniert nicht, Kopie kann nicht durchgeführt werden6 }

Die Lösung ist, das Objekt in einem „langlebigen“ Speicherbereich anzulegen und mit Verweisen darauf zu arbeiten. Genaueres erfahren Sie in Abschnitt 4.5.5. Zu­nächst müssen wir uns aber mit Zuweisungen befassen.

4.5.3.3 ZuweisungenAlles, was über Kopier-Konstruktoren gesagt wurde, gilt ähnlich auch für Zuweisun­gen, nur ist dabei der Kopier-Konstruktor nicht betroffen. Denn eine Zuweisung (3.4.1) ist keine Erstellung einer Kopie. Vielmehr wird der Inhalt eines Objekts in ein bestehendes Objekt kopiert. Da somit kein neues Objekt erstellt wird, kann der Kopier-Konstruktor keine Rolle spielen. Stattdessen geht es hier um den Zuwei­sungsoperator.

Sie können für Ihre Klasse die Semantik des Zuweisens über einen eigenen Zuwei­sungsoperator definieren. Genauso wie der Kopier-Konstruktor ist auch der Zuwei­sungsoperator für jede Klasse implizit definiert. Seine Implementierung ist einleuch­tend: Er kopiert Attribut für Attribut den Inhalt des Quell-Objekts in das Ziel-Objekt. Dabei wird bei objektwertigen Attributen wieder deren Zuweisungsoperator benutzt, während bei allen anderen eine bitweise Kopie angefertigt wird.

Bei einer Zuweisung wird der Zuweisungsoperator des Ziel-Objekts aufgerufen, weil dies das Objekt ist, das verändert wird. Dabei bekommt die Methode das Quell-Ob­jekt als Argument übergeben.

147

Kopier-Konstruk­tor muss nicht de­finiert werden

Lebensdauer von nicht kopierbaren Objekten

eine Zuweisung ist keine Initiali­sierung

eigener Zuwei­sungsoperator

Page 156: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

Der Zuweisungsoperator wird folgendermaßen deklariert:

Klassenname & operator = ( const Klassenname & Parametername );Das Schlüsselwort operator zusammen mit dem Gleichheitszeichen zeigt an, dass der Zuweisungsoperator gemeint ist. Der Rückgabetyp ist eine Referenz auf die zu­gehörige Klasse. Die Implementierung muss neben der entsprechenden Zuweisung der Attribute nämlich auch eine Referenz auf das eigene, veränderte Objekt zurück­liefern.

Beispiel:4 // Diese Klasse kapselt einen Wert.5 class Wert6 {7 public :8 // ... die alten Definitionen9

10 // der Zuweisungsoperator11 Wert &operator= (const Wert &quelle);1213 // ... die alten Definitionen14 };1516 Wert &Wert::operator= (const Wert &quelle)17 {18 wert = quelle.gibWert ();19 return *this; // gib Verweis auf das aktuelle Objekt zurück20 }

Einige Anmerkungen zum Programm:

• Zeile 18: Der Zuweisungsoperator in unserem Beispiel kopiert nur das Attribut wert; das Attribut original lässt er unangetastet. Die Idee dabei ist, dass ein Objekt durch Zuweisung seinen Status „Original“ oder „Kopie“ nicht verliert, weil es seine Identität beibehält. Ein Original bleibt somit ein Original, auch wenn ihm eine Kopie zugewiesen wird; und eine Kopie bleibt eine Kopie, auch wenn ihr irgendwann ein Original-Objekt zugewiesen wird.

• Zeile 19: Wie gesagt muss ein Zuweisungsoperator das veränderte Objekt zu­rückliefern. this ist ja ein Zeiger auf das Objekt, für das die Methode aufgeru­fen wurde (4.4.5.2); da wir aber nicht den Zeiger, sondern das Objekt, auf das der Zeiger verweist, benötigen, müssen wir den this-Zeiger mit Hilfe des *-Operators dereferenzieren (3.4.3.3).

Zum Schluss noch eine wichtige Anmerkung: Fast immer, wenn Sie einen benutzer­definierten (d. h. eigenen) Kopier-Konstruktor implementieren, müssen Sie auch einen entsprechenden benutzerdefinierten Zuweisungsoperator entwickeln. Denn beide sind für das Kopieren von Objekten zuständig, nur in unterschiedlichen Kontexten: Während beim Kopier-Konstruktor das Ziel-Objekt noch nicht existiert, ist es beim Zuweisungs­operator bereits da. Beide müssen jedoch Inhalte von Attributen kopieren (entweder durch Initialisierung oder durch Zuweisung). Deshalb bilden beide in der Regel ein en­ges Team mit ähnlicher Funktionalität.

Merksatz 21: Betrachte Kopierkonstruktor und Zuweisungsoperator als Team!

148

Form des Zuwei­sungsoperators

Ein Zuweisungs­operator gibt *this zurück

Kopier-Konstruk­tor und Zuwei­sungsoperator bilden ein Team

Page 157: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

4.5.4 Temporäre ObjekteWir haben in Zusammenhang mit Objekten noch ein wichtiges „Feature“ von C++ unterschlagen: temporäre Objekte. Temporäre Objekte sind unbenannte Objekte, de­ren Lebensdauer nur kurz („temporär“) ist und die nach ihrer Benutzung wieder ver­schwinden. In der nicht-objektorientierten Programmierung ist dieses Vorgehen gang und gäbe. Schauen Sie sich das folgende Beispiel einmal an:

1 int result = 1 + 42*23;Hier werden die Ganzzahl-Konstanten 1, 42 und 23 verwendet, ohne dass benannte Variablen oder Konstanten definiert und mit ihnen initialisiert werden.

Genauso wie bei primitiven Datentypen und den in C++ fest eingebauten Konstanten funktioniert das mit Objekten auch, nur die Syntax ist eine andere. Die Syntax für das Erzeugen und Benutzen temporärer Objekte ist ähnlich der Definition benannter Objekte, nur dass eben der Name fehlt:

Klassenname ( [Argument1 [, Argument2 [, ...]]] )Sie können sich die Erzeugung eines temporären Objekts auch so vorstellen, dass Sie den Konstruktor der Klasse wie eine Funktion aufrufen, der Ihnen dann ein Objekt der gewünschten Klasse zurück liefert. Dies ist übrigens ein Grund dafür, warum ein Konstruktor in C++ genauso heißt wie die zugehörige Klasse.

Wir wollen das Ganze an einem Beispiel verdeutlichen:1 /*** Beispiel tempobj.cpp ***/2 #include <ostream>3 #include <iostream>4 using namespace std;56 // kapselt eine Ganzzahl7 class Number8 {9 public :10 Number (int n);11 int Value () const;12 private :13 int m_n;14 };15 Number::Number (int n)16 :17 m_n (n)18 {19 }20 int Number::Value () const21 {22 return m_n;23 }2425 int main ()26 {27 // 1 + 42*2328 cout << Number(1).Value()29 + Number(42).Value() * Number(23).Value() << endl;30 return 0;31 }

149

Syntax für das Erzeugen tempo­rärer Objekte

Page 158: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

Die Klasse Number „verpackt“ int-Werte. Es gibt einen entsprechenden Konstruk­tor, der einen int-Wert in einem Number-Objekt zwischenspeichert, und eine Va­lue-Operation, die ihn wieder herausholt. Somit entsprechen Number-Objekte gan­zen Zahlen.

Uns interessiert hier jedoch weniger die Semantik40 der Klasse Number als vielmehr die Erzeugung und Benutzung von temporären Objekten. Dies sehen Sie in den Zei­len 28 und 29: Hier werden drei temporäre Number-Objekte erzeugt und verwendet.

Wozu aber überhaupt temporäre Objekte? Zuerst einmal: Man kann natürlich ohne sie auskommen und alle Objekte, mit denen man es zu tun hat, explizit als Variablen bzw. benannte Konstanten definieren. Allerdings ist es häufig der Fall, dass man Ob­jekte innerhalb eines Ausdrucks zur Berechnung eines Ergebnisses benötigt, die Ob­jekte aber sonst nirgendwo braucht. Ein Beispiel haben Sie bereits in Abschnitt 3.4.2.1 kennen gelernt:

4 string abba = string("AB")+string("BA");

Hier werden die Teil-Zeichenketten nach der Ausführung der Verkettung nicht mehr benötigt. Generell kann also gesagt werden, dass Sie temporäre Objekte immer dann benutzen können, wenn Sie die Dienste dieser Objekte nur kurzfristig „als Mittel zum Zweck“ einsetzen und anderweitig nicht benötigen.

Natürlich wird für ein temporäres Objekt neben dem obligatorischen Konstruktor auch der Destruktor aufgerufen, sobald das Objekt zerstört werden soll. Wann dies allerdings der Fall ist, ist etwas kompliziert. Generell kann man sagen, dass ein tem­poräres Objekt am Ende des enthaltenden Ausdrucks zerstört wird. Im obigen Bei­spiel werden also die Number-Objekte kurz vor dem Ausführen der return-An­weisung zerstört, da der enthaltende Ausdruck die Ausdrucks-Anweisung (3.5.1) in den Zeilen 28 und 29 ist.

Sie können temporäre Objekte an const-Referenzen (3.4.3.2) binden (mit nicht-con­st-Referenzen funktioniert das nicht, siehe hierzu auch Abschnitt 4.4.7.2). In einem solchen Fall gibt es die Regelung, dass das temporäre Objekt nicht wie gehabt am Ende des enthaltenden Ausdrucks zerstört wird; vielmehr wird dessen Lebensdauer (4.5.3) auf die Lebensdauer der Referenz ausgeweitet, an die es gebunden wird. Dadurch wird ver­hindert, dass die Referenz-Variable auf ein Objekt verweist, das nicht mehr existiert. Beispiel:

1 int square (int i)2 {3 const Number &result = Number(i*i);4 return result.Value ();5 }

Das temporäre Objekt, das in Zeile 3 erzeugt wird, wird an die Referenz-Variable re­sult gebunden; dadurch wird es erst zerstört, wenn die Lebensdauer von result er­lischt. Dies geschieht in diesem Beispiel am Ende der Funktion, wenn der Gültigkeits­bereich (3.3.4) der Referenz-Variable verlassen wird. Dadurch wird sichergestellt, dass die Nachricht Value in Zeile 4 an ein existierendes Objekt geschickt wird.

Sie sollten allerdings vermeiden, temporäre Objekte an const-Referenzen zu binden, wenn sich dies auch einfacher erledigen lässt. Beispiel:

40) Bedeutung

150

Wozu temporäre Objekte?

temporäre Objek­te und Referenzen

Page 159: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

1 const Number &n1 = Number(5);2 const Number &n2 = Number(n1.Value()*n1.Value());

Hier können Sie auch1 Number n1(5);2 Number n2(n1.Value()*n1.Value());

schreiben, was einfacher, übersichtlicher und meistens sogar performanter ist.

Der häufigste Grund, temporäre Objekte an const-Referenzen zu binden, ist bei der Parameterübergabe (siehe hierzu die Abschnitte 3.6.4 und 4.4.7.2).

4.5.5 Dynamischer Speicher

4.5.5.1 Speicher belegen und freigebenIn diesem Abschnitt lösen wir das Problem der kurzlebigen Objekte. Bisher war es so, dass unsere Objekte immer nur in dem Block existieren konnten, in dem sie defi­niert wurden (4.5.3). Wir lernen jetzt eine Möglichkeit kennen, Objekte ohne eine zugehörige Definition zu erzeugen. Somit fällt die Begrenzung der Lebensdauer au­tomatisch weg.

Es gibt einen Operator in C++, der Objekte in einem gesonderten Speicherbereich, dem Freispeicher oder Heap erzeugt: den Operator new. Ein Ausdruck mit dem new-Operator hat folgenden Aufbau:

new Typname [ ( Initialisierungsargumente ) ]

Das Ergebnis eines solchen Ausdrucks ist ein Zeiger (3.4.3.3) auf das neu erzeugte Objekt. Der Typ des Ergebnisses ist folglich Typname *.

Das Objekt existiert solange, bis es wieder explizit über den delete-Operator frei­gegeben wird:

delete Ausdruck

Dabei muss Ausdruck ein Zeiger sein, und zwar ein Zeiger, der vorher vom new-Operator zurückgeliefert wurde.

Auf das erzeugte Objekt kann normal zugegriffen werden, sobald der Zeiger darauf über die Operatoren * oder -> (3.4.3.3) dereferenziert wurde.

Beispiel:1 /*** Beispiel newdelete.cpp ***/2 #include <ostream>3 #include <iostream>4 using namespace std;56 // Diese Klasse repräsentiert einen einfachen Zähler.7 class Zaehler8 {9 public :10 // Konstruktor. Initialisiert den Zähler mit übergebenem Startwert11 Zaehler (int startwert);12 // gibt den aktuellen Wert des Zählers zurück13 int gibWert () const;14 // erhöht den Zähler um Eins

151

Freispeicher und Operator new

delete ist Ge­genstück zu new

Zugriff auf das Freispeicher-Ob­jekt

Page 160: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

15 void erhoehe ();16 // addiert „delta“ zum Zähler17 void addiere (int delta);18 private :19 int wert;20 };2122 Zaehler::Zaehler (int startwert)23 :24 wert (startwert)25 {26 }2728 int Zaehler::gibWert () const29 {30 return wert;31 }3233 void Zaehler::erhoehe ()34 {35 addiere (1);36 }3738 void Zaehler::addiere (int delta)39 {40 wert += delta;41 }4243 int main ()44 {45 Zaehler *zaehler = new Zaehler (1);46 cout << "Wert des Zählers: " << zaehler->gibWert () << endl;47 zaehler->addiere (1);48 cout << "Wert des Zählers: " << zaehler->gibWert () << endl;49 (*zaehler).addiere (-2);50 cout << "Wert des Zählers: " << (*zaehler).gibWert () <<endl;51 delete zaehler;52 return 0;53 }

Dieses Beispiel entspricht dem Beispiel in Abschnitt 4.5.1, bloß dass das Zaehler-Objekt jetzt auf dem Freispeicher liegt. Die Ausdrücke und Anweisungen, welche die Verwendung von Objekten im Freispeicher betreffen, sind nachfolgend erläutert.

• Zeile 45: Hier wird ein Objekt mit Hilfe des Operators new auf dem Freispei­cher erzeugt und der Zeiger darauf in der Variable zaehler hinterlegt.

Der Zeiger ist die einzige Möglichkeit, an das erzeugte Objekt heranzukommen. Deshalb ist es wichtig, ihn nicht zu „verlieren“, bevor das Objekt via delete zer­stört und der Speicher wieder freigegeben wird.

• Zeile 46-48: In diesen Zeilen wird mit dem Operator -> auf die Operationen des Objekts zugegriffen.

• Zeile 49-50: Dito, nur mit den *- und .-Operatoren. Beide Arten des Zugriffs sind vollkommen äquivalent, nur ist die erste kürzer und benötigt keine Klamme­rung.

152

Page 161: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

• Zeile 51: Hier wird das in Zeile 45 erzeugte Objekt wieder zerstört und der zuge­ordnete Speicherbereich wieder freigegeben.

Der Operator new alloziert (belegt) nicht nur den Speicher für das Objekt, er ruft da­nach auch den entsprechenden Konstruktor auf und übergibt ihm die Initialisierungs­argumente. Analog dazu ruft der delete-Operator den Destruktor auf, bevor er den Speicherbereich wieder freigibt. Die Konstruktoren und Destruktoren werden also genauso aufgerufen wie bei lokal (3.3.4) definierten Objekten.

Es kann nicht genug gesagt werden: Achten Sie immer darauf, dass Sie alle Objekte, die Sie mit new erzeugen, auch wieder mit delete zerstören. Wenn Sie dies vergessen, kann dies zu allerlei Fehlern führen:

(1) Der erste und offensichtliche Fehler ist Speicher-Verlust. Der Speicher, der für das nicht freigegebene Objekt reserviert ist, bleibt bis zur Beendigung des Programms für andere Objekte unerreichbar. Wenn Sie regelmäßig Verweise auf Objekte „ver­gessen“ und den Speicher nicht freigeben, wird der Speicher irgendwann volllau­fen und das Programm wegen Speichermangel abbrechen.

(2) Der zweite und subtilere Fehler ist Ressourcen-Verlust. Neben dem Speicher kann ein Objekt noch andere System-Ressourcen verwalten, etwa offene Dateien, offene Netzwerkverbindungen, Sperren etc. Wird ein Objekt nicht zerstört, wird sein De­struktor nicht aufgerufen, und die Ressourcen werden folglich nicht zurückgege­ben bzw. ordentlich „entsorgt“. Da jedes System über Grenzen verfügt (etwa die maximale Anzahl an geöffneten Dateien pro Prozess), können diese Grenzen bald erreicht sein, wenn Sie häufig ihre Objekte einfach „vergessen“

Um eine variable Anzahl an Objekten zu erzeugen, zu verwalten und wieder freizuge­ben, verwenden Sie am besten eine der Container-Klassen aus der C++-Standard-Bi­bliothek, wie vector oder list. Diese verwenden intern die new- und delete-Operatoren, so dass sich der Programmierer darum nicht mehr kümmern muss.

C++ verfügt standardmäßig nicht über einen sogenannten Garbage Collector, d. h. eine Komponente der Laufzeit-Umgebung, die automatisch Objekte zerstört und deren Spei­cher freigibt, auf die es keine Verweise (sprich: Zeiger und Referenzen) mehr gibt. Sie sind selbst dafür verantwortlich, die von Ihnen angeforderten Ressourcen auch wieder freizugeben, und das beinhaltet neben System-Ressourcen wie offene Dateien auch den Speicher der verwendeten Objekte, wenn sie nicht mehr gebraucht werden.

4.5.5.2 Der „leere“ VerweisWenn Objekte auf dem Freispeicher erzeugt werden, werden Verweise auf sie in Zei­ger-Variablen gespeichert. Nun kann es sein, dass eine Zeiger-Variable zwar initiali­siert werden muss (etwa im Konstruktor einer Klasse, wenn es sich um ein Attribut handelt), aber zu diesem Zeitpunkt kein Objekt erstellt werden soll, sondern erst spä­ter.

Zu diesem Zweck gibt es in C++ den sogenannten Null-Zeiger. Er wird durch die Ganzzahl-Konstante 0 dargestellt und steht für einen Zeiger, der ins „Nichts“ ver­weist. Ein Null-Zeiger ist mit jedem Zeiger-Typ verträglich und kann jeder Zeiger-Variable zugewiesen werden.

Achtung: Der Zeiger verweist wirklich ins „Nichts“! Wenn Sie einen solchen Null-Zei­ger dereferenzieren, d. h. versuchen, das nicht vorhandene Objekt „hinter“ dem Zeiger zu nutzen, wird Ihr Programm mit hoher Wahrscheinlichkeit abstürzen. Gehen Sie also doppelt und dreifach sicher, dass sie auf kein Objekt über einen Null-Zeiger zuzugreifen versuchen!

153

new/delete und Konstrukto­ren/Destruktoren

Freispeicher-Ob­jekte müssen ex­plizit freigegeben werden!

Container zur Verwaltung dyna­misch erzeugter Objekte nutzen

kein Garbage Collector in C++

Verweis ins Nichts: der Null-Zeiger

Null-Zeiger sind gefährlich!

Page 162: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

Weil Null-Zeiger so gefährlich sind, gibt es viele, die den Einsatz von Null-Zeigern generell ablehnen. Stattdessen schlagen sie den Einsatz von speziellen Objekten vor, die genauso aussehen wie die „richtigen“, aber auf jede Operation mit einer Ausnah­me reagieren. Sie repräsentieren also so etwas wie Null-Objekte, haben aber den Vorteil, dass das Programm beim Zugriff nicht abstürzt, sondern eine Ausnahme aus­wirft, die geeignet behandelt werden kann.

Merksatz 22: Vermeide Null-Zeiger, wo es nur geht!

4.5.5.3 Wenn kein Speicher mehr da istSelbst wenn Sie auf dem größten und teuersten Rechner-System arbeiten: Speicher ist immer endlich. Es ist möglich, dass ihr Programm an die Speicher-Grenzen des Systems stößt. Wenn Sie versuchen, ein Objekt mit dem Operator new zu erzeugen und kein Speicher mehr zur Verfügung steht, wird von der C++-Laufzeit-Umgebung ein Objekt der Klasse std::bad_alloc (definiert in der Header-Datei <new>) erzeugt und als Ausnahme ausgeworfen. Diese Ausnahme können Sie auffangen und geeignet behandeln. Neben einem ziemlich drastischen Programmabbruch ist eine sinnvolle Alternative, wenig benötigte Objekte (die z. B. als Zwischenspeicher fun­gieren) freizugeben.

Das Ausnahme-Objekt wird selbst natürlich nicht auf dem Freispeicher erstellt, weil das zu einer unendlichen Fehler-Kette führen würde. Jede C++-Implementierung reserviert am Programm-Anfang einen speziellen Speicherbereich, der in solchen Situationen der extremen Speicher-Belastung für Ausnahme-Objekte benutzt wird.

In VC++ funktioniert der Mechanismus mit dem Auswerfen einer Ausnahme bei Spei­chermangel nicht automatisch. Er muss erst geeignet aktiviert werden. Sie sollten in je­dem VC++-Programm die folgende Datei in Ihr Projekt einbinden, der Code zur Ver­wendung von C++-Ausnahmen bei zu wenig Speicher ermöglicht:

1 /*** VC++-Erweiterung vcppnew.cpp ***/2 /*** stellt sicher, dass bei Speichermangel eine Ausnahme ausgeworfen wird ***/3 #include <new> // für std::bad_alloc4 #include <new.h> // für set_new_handler5 using namespace std;67 #pragma warning (disable:4073) // Warnung unterdrücken8 #pragma init_seg(lib) // frühe Initialisierung des globalen Objekts9

10 // kapselt Logik zum Setzen eines neuen new-Handlers11 class BadAllocActivator12 {13 public :14 // Konstruktor: setzt den neuen new-Handler und merkt sich den alten15 // in oldNewHandler16 BadAllocActivator ();17 // Destruktor: restauriert den alten new-Handler18 ~BadAllocActivator ();19 private :20 // Attribut speichert alten new-Handler21 _PNH oldNewHandler;22 // neuer new-Handler23 static int newNewHandler (size_t size);24 };25

154

wenn der Spei­cher knapp wird

VC++ braucht etwas „Nachhil­fe“

Page 163: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

26 BadAllocActivator::BadAllocActivator ()27 {28 // verwende VC++-eigene Routine zum Setzen des new-Handlers29 oldNewHandler = _set_new_handler (newNewHandler);30 }3132 BadAllocActivator::~BadAllocActivator ()33 {34 // verwende VC++-eigene Routine zum Setzen des new-Handlers35 _set_new_handler (oldNewHandler);36 }3738 // Parameter wird nicht benötigt, deswegen ist sein Name auskommentiert39 int BadAllocActivator::newNewHandler (size_t /*size*/)40 {41 // wirf Ausnahme aus42 throw bad_alloc ();43 }4445 // globales Objekt, das noch vor der Ausführung von main initialisiert wird und46 // auch erst nach der Beendigung von main zerstört wird47 BadAllocActivator badAllocActivator;

Dieses Modul installiert, noch bevor die ersten Anweisungen der Funktion main aufge­rufen werden, eine neue VC++-interne Fehlerbehandlungs-Funktion für den new-Ope­rator, die eine Ausnahme bei zu wenig Speicher auswirft. Das Skript kann leider nicht auf alle Sprachmittel eingehen, die hier verwendet werden; nehmen Sie es einfach hin, dass der obige Programm-Code funktioniert.

4.5.5.4 Dynamische DatenstrukturenDynamisch allozierte Objekte werden verwendet, wenn nicht von vornherein bekannt ist, wie viele Objekte benötigt werden. Häufig werden diese Objekte in dynamisch wachsenden und schrumpfenden Datenstrukturen abgelegt. Für diese Datenstruktu­ren ist es charakteristisch und notwendig, dass Zeiger verwendet werden, um auf die verwalteten Objekte zu verweisen.

Wir wollen eine kleine, einfach verkettete Liste als Grundlage eines Stapels zum Speichern von Zahlen entwickeln, um den Einsatz von dynamischen Datenstrukturen mit Hilfe von Zeigern vorzustellen. Allerdings sollten Sie immer im Hinterkopf ha­ben, dass die hier entwickelte Datenstruktur ein Beispiel ist, um gewisse C++-Sprachkonzepte zu verdeutlichen. Sie sollten stets die in der C++-Standard-Biblio­thek angebotenen Datenstrukturen (8.3) nutzen, wenn Sie dynamisch Objekte ver­walten wollen; diese sind reich an Algorithmen, optimiert, gut getestet, flexibel ein­setzbar und generisch (d. h. für alle möglichen Typen geeignet, nicht nur für Zahlen).

Zuerst klären wir, was ein Stapel ist. Ein Stapel ist ein Behälter, bei dem nur auf das Element zugegriffen werden kann, das zuletzt „darauf gelegt“ wurde. Wie bei einem richtigen Stapel ist dieses Element „ganz oben“. Die typischen Operationen für einen Stapel sind (Abbildung 34):

• push (drauflegen)

• pop (herunternehmen)

• top (oberstes Element inspizieren, ohne es wegzunehmen)

155

Wozu dynamische Objekte?

exemplarisch: verkettete Liste als dynamische Datenstruktur

Analyse und Ent­wurf

Page 164: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

• isEmpty (prüfen, ob der Stapel leer ist)

Dann klären wir, was eine einfach verkettete Liste ist. Eine solche Liste besteht aus Elementen, die sich gegenseitig kennen, und zwar kennt jedes Element seinen Nach­folger in der Liste. Kennt man somit den Anfang der Liste, kann man alle Elemente in der Liste finden, wenn man sich an den Elementen über die Nachfolger-Beziehung „entlanghangelt“. Abbildung 35 verdeutlicht dies anhand einer konkreten Liste mit drei Elementen.

Jetzt, wo wir wissen, wie eine Liste und ein Stapel funktionieren, können wir die Operationen und die Beziehungen zwischen den Klassen in einem Klassendiagramm zusammenfassend beschreiben (Abbildung 36).

156

Abbildung 35: Einfach verkettete Liste mit drei Elementen

element1: Element element2: Element

element3: Elementliste: Liste

anfang

Abbildung 34: Stapel-Operationen

element1 element1

element2

element1

top

push(leerer Stapel)

push pop

top

top

Abbildung 36: Entwurf einer Stapel-Klasse, die auf einer verketten Liste aufbaut

Liste

add(value: int)remove()getFirst(): intisEmpty(): bool

0..1

head

Element

value: intElement (value: int, next: Element)getValue(): intgetNext(): Element

0..1next

Stapel

push(value: int)pop()top(): intisEmpty(): bool

1impl

Page 165: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

Wir müssen noch die genaue Bedeutung der Operationen add und remove klären. Um es uns später einfach zu machen, sind diese Operationen folgendermaßen aufge­baut: Jedes neu hinzugefügte Element wird an den Anfang der Liste gestellt, jedes entfernte Element wird vom Anfang der Liste genommen. Somit ist unsere so ent­worfene Liste genauso wie der Stapel eine LIFO-Datenstruktur und für die Nutzung durch den Stapel geradezu prädestiniert.

Jetzt können wir fast den Entwurf eins-zu-eins in C++-Quelltext übertragen. Wir re­kapitulieren: Das Ziel des Entwurfs ist es, die technische Lösung darzustellen, und zwar möglichst so, dass alle Informationen zur Implementierung der beschriebenen Klassen und Methoden existieren. Unser Entwurf ist detailliert genug, um die Imple­mentierung relativ einfach „hinzuschreiben“. Wir wollen dies stückweise tun.

1 /*** Beispiel stapel.cpp ***/2 #include <ostream>3 #include <iostream>4 #include <string>5 using namespace std;6

Zuerst kommt der fast schon obligatorische Anfang.7 // stellt den Typ für die gespeicherten Werte dar8 typedef int ValueType;9

Hier wird der Typ definiert, der in den Elementen der Liste gespeichert wird. In un­serem Entwurf waren wir nicht vorausschauend genug und haben uns auf den Typ int festgelegt. Das ist natürlich unschön, und deshalb haben wir eine passende Ab­straktion (ValueType) gewählt.

10 // Kapselt ein Listen-Element.11 class Element12 {13 public :14 // Konstruktor. Setzt den Wert und den Zeiger auf das nächste Element.15 Element (ValueType theValue, Element *theNext);1617 // Liefert gespeicherten Wert zurück.18 ValueType getValue () const;1920 // Liefert Zeiger aufs nächste Element zurück.21 Element *getNext () const;22 private :23 ValueType value;24 Element *next;25 };26

Die Klasse Element wird definiert. Die Kommentare sollten detailliert genug sein, um die Bedeutung der Operationen zu verstehen. Zu beachten ist, dass Objekte der Klasse Element im Prinzip konstant sind, denn es gibt keinerlei Operationen, die zur Änderung von Attributen herangezogen werden könnten. Einmal durch den Kon­struktor initialisiert, behält eine Element-Instanz den gespeicherten Wert und die Verbindung zum Nachfolger-Element bei. Auch sind die Operationen zum Zugriff auf die Attribute const (4.4.7.1), weil sie das Objekt nicht verändern.

157

Implementierung

Typ-Abstraktion für zu speichern­de Werte

Element-Objek­te sind unverän­derlich

Page 166: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

27 Element::Element (ValueType theValue, Element *theNext)28 :29 value (theValue),30 next (theNext)31 {32 }3334 ValueType Element::getValue () const35 {36 return value;37 }3839 Element *Element::getNext () const40 {41 return next;42 }43

Die Implementierung der entsprechenden Operationen und des Konstruktors ist nicht weiter besonders aufregend.

44 // Implementiert eine einfach verkettete Liste.45 class Liste46 {47 public :48 // Konstruktor: erstellt leere Liste.49 Liste ();5051 // Fügt Wert am den Anfang der Liste ein.52 void add (ValueType value);5354 // Entfernt Wert vom Anfang der Liste.55 void remove ();5657 // Liefert Wert am Anfang der Liste.58 ValueType getFirst () const;5960 // Liefert true wenn Liste leer ist, false sonst.61 bool isEmpty () const;62 private :63 Element *head;64 };65

Die Definition einer Liste entspricht direkt unserem Entwurf. Die Operationen get­First und isEmpty sind beide const, da sie die Objekte unverändert lassen.

66 Liste::Liste ()67 :68 // kein Element am Anfang: Liste ist leer69 head (0)70 {71 }7273 void Liste::add (ValueType value)74 {75 // erstellt neues Element, verkettet es mit dem Element, das momentan76 // am Anfang steht, und stellt das neue Element selbst an den Anfang77 head = new Element (value, head);78 }7980 void Liste::remove ()

158

Page 167: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

81 {82 // speichere alten Anfang, setze Anfang auf das Element,83 // das ihm folgt, und zerstöre das alte Anfangs-Element84 Element *oldHead = head;85 head = head->getNext ();86 delete oldHead;87 }8889 ValueType Liste::getFirst () const90 {91 // frage Anfangs-Element92 return head->getValue ();93 }9495 bool Liste::isEmpty () const96 {97 // Liste ist leer, wenn kein Anfang existiert98 return head == 0;99 }

100

Dies ist die Implementierung der Operationen der Klasse Liste. Beachten Sie den Gebrauch der Element-Objekte, die auf dem Freispeicher erzeugt werden. In der Methode add wird in Zeile 77 für jedes neue Element ein Element-Objekt auf dem Freispeicher erzeugt und zum Anfang der Liste gemacht. Dabei wird der vorherige Anfang der Liste der Nachfolger des neuen Anfangs; somit ist der vorherige Anfang nun das zweite Element der Liste. Beim Einfügen des allerersten Elements gibt es keinen Anfang; in diesem Fall existiert kein Verweis auf das nächste Element und das neue Element ist gleichzeitig das Ende der Liste.

Die Methode remove entfernt das erste Element der Liste, indem es sich zuerst die­ses in einer lokalen Variable merkt und dann das Element hinter dem aktuellen An­fang zum ersten Element macht. Danach wird das ursprünglich am Beginn stehende und nun nicht mehr gebrauchte Element in Zeile 86 ordnungsgemäß zerstört und der belegte Speicher freigegeben.

101 // Implementiert einen Stapel.102 class Stapel103 {104 public :105 // packt Wert auf den Stapel106 void push (ValueType value);107108 // entfernt obersten Wert vom Stapel109 void pop ();110111 // liest obersten Wert vom Stapel (lässt ihn aber dort)112 ValueType top () const;113114 // liefert true wenn Stapel leer ist, false sonst.115 bool isEmpty () const;116 private :117 Liste impl;118 };119

159

add und new

remove und delete

Page 168: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

Die Definition der Klasse Stapel entspricht ebenfalls direkt unserem Entwurf. Beach­ten Sie die Zeile 117: Die Zu-eins-Assoziation impl wurde als einfaches Attribut umgesetzt. Dies ist typisch für C++ und auch andere Sprachen: Zu-eins-Assoziatio­nen und Attribute sind sich sehr ähnlich (4.4.4). Assoziationen hingegen, die auf vie­le Elemente verweisen (etwa 0..* oder 1..*) können nur durch geeignete Datenstruk­turen abgebildet werden. Solche Datenstrukturen sind ja auch gerade die von uns entwickelten Klassen Liste und Stapel.

120 void Stapel::push (ValueType value)121 {122 impl.add (value);123 }124125 void Stapel::pop ()126 {127 impl.remove ();128 }129130 ValueType Stapel::top () const131 {132 return impl.getFirst ();133 }134135 bool Stapel::isEmpty () const136 {137 return impl.isEmpty ();138 }139

Die Implementierung der Operationen der Klasse Stapel gestaltet sich sehr ein­fach: Wir delegieren alle Operationen an das Element impl. (impl steht übrigens für Implementierung und zeigt an, dass die Dienste der Klasse Stapel mit Hilfe der Klasse Liste implementiert sind.)

140 // Zeigt Informationen zum Status des Stapels an:141 // 1) leer oder nicht leer142 // 2) wenn nicht leer: oberstes Element143 // Eingabe-Parameter:144 // "stapel": Referenz auf einen Stapel145 // "nachricht" wird vor den anderen Informationen als zusätzliche Information ausgegeben146 void zeigeStapelInfo147 (const string &nachricht, const Stapel &stapel)148 {149 bool empty = stapel.isEmpty ();150 cout151 << nachricht << ": \n"152 << " Stapel ist: "153 << (empty ? "leer" : "nicht leer") << endl;154155 if (!empty)156 cout << " oben liegt: " << stapel.top () << endl;157 }158

Diese Funktion hilft uns, die Implementierung der Klasse Stapel zu testen. Sie gibt Informationen über den Zustand des Stapels (leer/nicht leer, oberstes Element falls vorhanden) über den cout-Ausgabe-Strom aus.

160

Assoziationen und Attribute

Delegation

Test

Page 169: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

159 int main ()160 {161 Stapel stapel;162 zeigeStapelInfo ("nach Konstruktor", stapel);163 stapel.push (1);164 zeigeStapelInfo ("nach push(1)", stapel);165 stapel.push (2);166 zeigeStapelInfo ("nach push(2)", stapel);167 stapel.push (3);168 zeigeStapelInfo ("nach push(3)", stapel);169 stapel.pop ();170 zeigeStapelInfo ("nach pop", stapel);171 stapel.pop ();172 zeigeStapelInfo ("nach pop", stapel);173 stapel.pop ();174 zeigeStapelInfo ("nach pop", stapel);175176 return 0;177 }

Unser Hauptprogramm erstellt in Zeile 161 ein (lokales) Objekt der Klasse Stapel und führt ein paar Operationen auf diesem Objekt aus. Die durchgeführten Ausgaben sind (falls Sie alles richtig gemacht haben):

nach Konstruktor: Stapel ist: leernach push(1): Stapel ist: nicht leer oben liegt: 1nach push(2): Stapel ist: nicht leer oben liegt: 2nach push(3): Stapel ist: nicht leer oben liegt: 3nach pop: Stapel ist: nicht leer oben liegt: 2nach pop: Stapel ist: nicht leer oben liegt: 1nach pop: Stapel ist: leer

4.6 Abstrakte Datentypen: Erweiterbarkeit und Flexibilität erhöhenIn diesem Abschnitt lernen Sie, wie Sie Gemeinsamkeiten beim Verhalten von Ob­jekten in C++ ausdrücken. Sie lernen, Schnittstellen und Implementierung voneinan­der zu trennen und erfahren, wie Sie daraus konkret Nutzen ziehen können.

4.6.1 (Abstrakte) Operationen und abstrakte KlassenEine Operation ist, wie Sie in Abschnitt 4.1 gelernt haben, eine Spezifikation eines Dienstes. Eine abstrakte Operation ist eben genau dies – sie sagt aus, was getan wird, aber nicht, wie es getan wird. Es existiert keine Methode in der betreffenden Klasse, die diesen Dienst auch implementiert.

161

Benutzung

Wiederholung: abstrakte Opera­tionen

Page 170: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

Ein kleines Beispiel zum Verständnis: Betrachten wir einmal ein System zur Mani­pulation von geometrischen Figuren. Dieses System lässt den Benutzer u. a. Linien41, Rechtecke und Kreise erzeugen und sie geeignet verändern. Sie können sich unser System also als einen Editor für (einfache) Graphiken vorstellen.

Zuerst einmal ist es klar, dass wir für jede Art eines solchen graphischen Objekts eine eigene Klasse einführen. Wir haben also die Klasse Line zum Kapseln von Li­nien, die Klasse Rectangle zum Kapseln von Rechtecken und die Klasse Circle zum Kapseln von – na, was denken Sie? – Kreisen.

Was macht ein solches graphisches Objekt aus? Nun, zuerst hat jedes Objekt augen­scheinlich gewisse Eigenschaften oder Attribute:

• Linie (Line): wird durch Start- und Endpunkt bestimmt

• Rechteck (Rectangle): wird durch zwei Eckpunkte bestimmt, die einander diagonal gegenüberliegen

• Kreis (Circle): wird durch Mittelpunkt und Radius bestimmt

Allen ist erst einmal gemein, dass sie Punkte zur Beschreibung der Position benöti­gen. Ein Punkt ist also ein weiteres Konzept, das als Klasse ausgedrückt werden soll­te:

• Punkt (Point): wird durch eine X- und eine Y-Koordinate bestimmtHieran können Sie erkennen, wie wichtig eine gründliche Analyse eines gestellten Pro­blems ist. In der ursprünglichen Aufgabenstellung spielen Punkte keine Rolle. Nichtsde­stoweniger müssen Sie sich mit ihnen auseinandersetzen, weil die zu unterstützenden Figuren allesamt mit Punkten „zu tun haben“. Eine eigene Klasse für Punkte einzufüh­ren ist wichtig, da sie die Kapselung von Eigenschaften eines Punktes ermöglicht.

Der letzte Satz bedarf einer Erklärung. Stellen Sie sich vor, Sie haben keine Klasse für Punkte eingeführt. Dann haben Sie in der Linien-Klasse wahrscheinlich Attribute wie startX und startY bzw. endX und endY, um den Start- und End-Punkt geeignet abzuspeichern. Ähnlich wird es in der Rechteck- und der Kreis-Klasse aussehen.

Jetzt kommt der Kunde (!) und fordert die Erweiterung des Systems auf den dreidimen­sionalen Raum. Sie müssen nun in allen Klassen, die von Koordinaten abhängen, ein zu­sätzliches Attribut für die Z-Koordinate einführen (z. B. startZ und endZ in der Li­nien-Klasse). Sie müssen sich in allen diesen Klassen darum kümmern, dass diese neuen Attribute geeignet initialisiert werden, eventuell müssen Sie Parameter zu Operationen hinzufügen, die mit Punkten operieren u. s. w. Sie haben also eine Menge zu tun.

Hätten Sie in diesem Fall von vornherein die Abstraktion Punkt erkannt und durch eine Klasse ausgedrückt, wäre Ihnen dies alles erspart geblieben. Alles, was Sie in diesem Fall tun müssen, ist ein entsprechendes Attribut für die Z-Koordinate zu der Punkt-Klas­se hinzuzufügen und in dieser Klasse die Schnittstelle (Konstruktor, Zugriffs- und ver­ändernde Operationen) entsprechend anzupassen. Alle Punkt-Klienten sind dadurch au­tomatisch für den dreidimensionalen Raum gewappnet.42

41) genauer: Strecken42) Natürlich ist es mit dem Hinzufügen einer dritten Koordinate nicht getan: Die Algorithmen müssen

ebenfalls angepasst werden. Aber dies kann lokal geschehen, da diese Algorithmen (etwa der Ab­stand zweier Punkte) sicherlich Teil der Schnittstelle der Klasse Point ist, während im anderen Fall (keine eigene Klasse Point) die Algorithmen quer über das gesamte Programm verteilt sein könnten (und werden).

162

Beispiel: System für graphische Objekte

Eigenschaften und Zustände sind verschieden

Page 171: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

Diese Attribute haben alle eine unterschiedliche Funktion und sind somit in den ein­zelnen Klassen gut aufgehoben. Was die Klassen (inklusive der Klasse Point, die wir hinzugefügt haben) aber verbindet, ist die Schnittstelle, also wie ich mit den Ob­jekten umgehe. Beispielsweise macht in dem Editor die folgende Schnittstelle Sinn:

• Anzeigen eines Objekts

• Verstecken eines Objekts

• Verschieben eines Objekts

• Drehen eines Objekts

• Vergrößern/Verkleinern eines Objekts

• u. v. a. m.

Natürlich ist die Realisierung dieser Operationen unterschiedlich: Ein Rechteck wird anders gedreht als eine Linie. (Oder haben Sie schon mal einen Kreis gedreht?) Wie die Operationen implementiert sind, ist aber dem Editor und letztlich dem Anwender so ziemlich egal. Alles was er will ist dem Objekt zu sagen, dass es sich bewegen, drehen, anzeigen oder verstecken soll. Wie das genau passiert, ist nicht wichtig.

Diese Gemeinsamkeiten in der Benutzung oder Schnittstelle der Objekte fasst man in einer abstrakten Klasse oder Schnittstellen-Klasse zusammen. In C++ gibt es keinen Unterschied zwischen (reinen) Schnittstellen-Klassen (keine Attribute, keine Metho­den, nur Operationen) und abstrakten Klassen (mindestens eine Operation ohne Me­thode, kann Attribute und Methoden enthalten); beiden ist gemein, dass sie mindes­tens eine abstrakte Operation (d. h. Operation ohne Methode) besitzen. Eine solche Operation wird in C++ dadurch gekennzeichnet, dass ihr

(1) der virtual-Modifizierer vorangestellt und

(2) sie mit dem „Wert“ Null „initialisiert“ wird.

Beispiel:1 /*** Beispiel graphobj1.cpp ***/2 #include <ostream>3 #include <iostream>4 using namespace std;56 // Kapselt ein graphisches Objekt.7 class GraphicalObject8 {9 public :10 // zeigt das Objekt an11 virtual void show () = 0;1213 // versteckt das Objekt14 virtual void hide () = 0;1516 // verschiebt das Objekt um „deltaX“ Pixel nach rechts und um „deltaY“ Pixel nach unten;17 // negative Werte ändern die Richtung der Verschiebung18 virtual void move (int deltaX, int deltaY) = 0;1920 // weitere mögliche Operationen weggelassen21 };

163

Schnittstelle ist gleich

Verhalten ist un­terschiedlich

Schnittstellen-Klassen und ab­strakte Klassen

Page 172: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

22Es gibt einige Unterschiede zwischen abstrakten Klassen und Schnittstellen in Java und C++:

• In Java müssen abstrakte Klassen mit einem Schlüsselwort (abstract) markiert werden.

• In Java kann eine Klasse abstrakt sein, ohne dass eine einzige Operation darin ab­strakt ist. In C++ lässt sich dies nur dadurch nachbilden, dass solch eine Klasse nur protected-Konstruktoren besitzt; dadurch können keine Instanzen dieser Klasse von Klienten erzeugt werden.

• In Java gibt es den Unterschied zwischen Schnittstellen (interface) und ab­strakten Klassen (abstract). Dies ist in Java erforderlich, da Java keine Mehr­fachvererbung (4.6.5) kennt und eine Java-Klasse nur höchstens von einer (ab­strakten oder konkreten) Klasse erben, aber viele Schnittstellen implementieren kann. C++ erlaubt Mehrfachvererbung, so dass in C++ formal kein Unterschied zwischen Schnittstellen-Klassen und abstrakten Klassen existiert.

• In Java müssen alle Operationen einer Schnittstellen-Klasse öffentlich (public) sein; in C++ gibt es diese Beschränkung nicht.

Das resultierende UML-Diagramm sehen Sie in Abbildung 37.

4.6.2 Implementierung von SchnittstellenZu einer solche Schnittstellen-Klasse oder abstrakten Klasse wie im letzten Abschnitt kann man natürlich keine Objekte erzeugen. Es gibt kein konkretes „graphisches Ob­jekt“, nur Punkte, Linien, Kreise, Rechtecke u. s. w. Somit können wir nicht

1 // Kein gültiges C++!2 int main ()3 {4 GraphicalObject object;5 object.show ();6 return 0;7 }

oder1 // Kein gültiges C++!2 int main ()3 {4 GraphicalObject *object = new GraphicalObject;5 object->show ();6 delete object;7 return 0;8 }

164

es gibt keine „ab­strakten Objekte“

Abbildung 37: GraphicalObject-Schnitt­stelle

<<interface>>GraphicalObject

void show()void hide()void move (int deltaX, int deltaY)

Page 173: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

schreiben. GraphicalObject ist lediglich eine Abstraktion, eine Verallgemeine­rung der Konzepte Punkt, Linie, Rechteck und Kreis in Bezug auf die Operationen, die man mit ihnen durchführen kann.

Dennoch ist es nützlich, diese Abstraktion zu verwenden. Bloß müssen die Operatio­nen dieser Schnittstelle implementiert, mit Leben gefüllt werden. Die Schnittstellen-Klasse GraphicalObject kann dies aus den o. g. Gründen nicht leisten, die kon­kreten Klassen Point (Punkt), Line (Linie), Rectangle (Rechteck) oder Circle (Kreis) schon. Betrachten wir zunächst die Klasse Point:

23 // kapselt einen Punkt in der Ebene24 class Point : public GraphicalObject25 {26 public :27 // konstruiert einen Punkt aus einer X- und einer Y-Koordinate28 Point (int x, int y);2930 // liefert die jeweiligen Koordinaten zurück31 int getX () const;32 int getY () const;3334 // Methoden zu den Operationen aus der Schnittstelle GraphicalObject35 virtual void show ();36 virtual void hide ();37 virtual void move (int deltaX, int deltaY);3839 private :40 int m_x;41 int m_y;42 };43 44 Point::Point (int x, int y) : m_x (x), m_y (y) {}45 int Point::getX () const {return m_x;}46 int Point::getY () const {return m_y;}4748 void Point::show ()49 {50 cout << "Point (" << m_x << ", " << m_y << ") shown." <<

endl;51 }5253 void Point::hide ()54 {55 cout << "Point (" << m_x << ", " << m_y << ") hidden." <<

endl;56 }5758 void Point::move (int deltaX, int deltaY)59 {60 m_x += deltaX;61 m_y += deltaY;62 }63

Neu ist die Konstruktion in Zeile 24. Über die Syntax

: public Klassenname

165

Schnittstelle wird implementiert

Syntax der Spezi­alisierung

Page 174: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

erfolgt eine Spezialisierung der Schnittstelle oder Klasse Klassenname. Dadurch übernimmt die konkretere Klasse (hier: Point) alle Operationen, Attribute und Me­thoden der allgemeineren (hier: GraphicalObject). Im Falle von reinen Schnitt­stellen-Klassen werden natürlich nur (abstrakte) Operationen übernommen.

Konstruktoren und Destruktoren werden nicht vererbt. Sie müssen jeden Konstruktor ei­ner Klasse definieren, ansonsten wird nur der Default-Konstruktor mit leerer Parameter­liste (4.5.1) sowie der Kopier-Konstruktor (4.5.3.1) generiert. Gleiches gilt für den De­struktor. Konstruktoren und Destruktoren der Oberklassen werden aber immer implizit oder explizit aufgerufen, s. u.; dies unterscheidet sie von normalen vererbten Methoden. Zur Vererbung von Methoden siehe Abschnitt 4.6.4.

Diese Operationen können dann in der spezielleren Klasse implementiert werden. Im obigen Fall implementiert Point alle abstrakten Operationen der Schnittstelle GraphicalObject; da sie danach überhaupt keine abstrakten Operationen mehr enthält, ist sie eine konkrete Klasse. Damit ist es erlaubt, Point-Objekte zu erzeu­gen. Die Implementierung der abstrakten Operationen unterscheidet sich nicht von der Definition von „normalen“ Methoden; insbesondere muss das Schlüsselwort virtual, das in der Deklaration noch vorhanden ist, hier weggelassen werden (Zei­len 48, 53, 58).

Implementiert eine speziellere Klasse nicht alle Operationen der allgemeineren, so bleibt sie eine abstrakte Klasse. Der Spezialfall, dass sowohl die allgemeinere wie auch die speziellere Klasse beide nur abstrakte Operationen definieren und somit eine Schnittstellen-Erweiterung vorliegt (4.1.6), ist natürlich auch möglich.

166

partielle Imple­mentierung und Schnittstellen-Er­weiterung

Abbildung 38: GraphicalObject-Hierarchie

<<interface>>GraphicalObject

void show ()void hide ()void move (int deltaX, int deltaY)

Point

Point (int x, int y)int getX () constint getY () constvoid show ()void hide ()void move (int deltaX, int deltaY)

int m_xint m_y

<<realize>>

Line

Line (Point startPoint, Point endPoint)Point getStartPoint () constPoint getEndPoint () constvoid show ()void hide ()void move (int deltaX, int deltaY)

<<realize>>

m_startPoint m_endPoint

Page 175: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

Ähnlich wird auch die Klasse Line definiert. Auf die Definition der Klassen Rec­tangle und Circle verzichten wir aus Platzgründen. Das zugehörige UML-Klas­sendiagramm sehen Sie in Abbildung 38.

64 // kapselt eine Linie65 class Line : public GraphicalObject66 {67 public :68 // konstruiert eine Linie aus einem Start- und einem Endpunkt69 Line (Point startPoint, Point endPoint);7071 // liefert Startpunkt72 Point getStartPoint () const;73 // liefert Endpunkt74 Point getEndPoint () const;7576 // Methoden zu den Operationen aus der Schnittstelle GraphicalObject77 virtual void show ();78 virtual void hide ();79 virtual void move (int deltaX, int deltaY);8081 private :82 Point m_startPoint; // der Startpunkt83 Point m_endPoint; // der Endpunkt84 };8586 Line::Line (Point startPoint, Point endPoint)87 :88 m_startPoint (startPoint),89 m_endPoint (endPoint)90 {91 }9293 Point Line::getStartPoint () const {return m_startPoint;}94 Point Line::getEndPoint () const {return m_endPoint;}9596 void Line::show ()97 {98 cout << "Showing line:" << endl;99 cout << " "; m_startPoint.show ();

100 cout << " "; m_endPoint.show ();101 cout << "Line shown." << endl;102 }103 void Line::hide ()104 {105 cout << "Hiding line:" << endl;106 cout << " "; m_startPoint.hide ();107 cout << " "; m_endPoint.hide ();108 cout << "Line hidden." << endl;109 }110111 void Line::move (int deltaX, int deltaY)112 {113 m_startPoint.move (deltaX, deltaY);114 m_endPoint.move (deltaX, deltaY);115 }116

Das Hauptprogramm erzeugt einige Objekte und schickt ihnen zum Testen ein paar Nachrichten:

167

Page 176: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

117 int main ()118 {119 Point p1 (1, 2);120 Point p2 (3, 5);121 Line l (p1, p2);122 l.show ();123124 Point p3 (6, 8);125 p3.show ();126 p3.hide ();127 p3.move (1, 1);128 p3.show ();129130 return 0;131 }

Die Ausgabe des Programms lautet:Showing line: Point (1, 2) shown. Point (3, 5) shown.Line shown.Point (6, 8) shown.Point (6, 8) hidden.Point (7, 9) shown.

4.6.3 PolymorphieIm letzten Abschnitt haben wir mit der Schnittstellen-Klasse GraphicalObject eine Gleichartigkeit in der Benutzung von Punkten und Linien ausgedrückt. Bisher haben wir jedoch diese Gleichartigkeit nicht ausgenutzt: Wenn wir einem Punkt oder einer Linie eine Nachricht schickten, wussten wir (der Übersetzer eingeschlossen), dass es sich um einen Punkt oder eine Linie handelt.

Durch die Schnittstelle GraphicalObject können wir jedoch nun einen Algorith­mus niederschreiben, der von der konkreten Klasse eines Objekts nichts wissen muss. Solange alles, was der Algorithmus benötigt, in der Schnittstelle Graphi­calObject enthalten ist, muss er keine weiteren Annahmen über die Klasse eines Objektes treffen. Das ist gut, weil der Algorithmus ansonsten in einer Fallunterschei­dung (beispielsweise in einer if-Anweisung) das Verhalten für jede konkrete Klasse wählen müsste:

1 if ( Objekt ist vom Typ Point ) {2 // tu etwas3 } else if ( Objekt ist vom Typ Line ) {4 // tu etwas anderes5 } else {6 // ??? was tun???7 }

Dies ist schlecht, da der Algorithmus jede konkrete Klasse kennen muss. Fügen wir in unser Programm beispielsweise die Klasse Ellipse hinzu, muss der obige Ab­schnitt um eine if-Abfrage erweitert werden. Schlimmer noch: Jeder Algorithmus, der auf solchen Objekten arbeitet, wird eine solche Fallunterscheidung haben (müs­sen), somit wird diese Änderung an vielen verschiedenen Stellen erfolgen müssen. Die Erweiterbarkeit eines solchen Programms ist somit stark eingeschränkt.

168

Gleichartigkeit bisher nicht aus­genutzt

Unabhängigkeit von der konkreten Klasse ist gut

Nicht-Verwenden von Abstraktio­nen führt zu nicht wartbarem Code

Page 177: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

Eine andere fragliche Sache ist oben mit drei Fragezeichen markiert (???). Was ist, wenn das Objekt von einer Klasse ist, die der Algorithmus nicht kennt? Dann kann kein sinnvolles Verhalten erfolgen, es liegt also ein Fehler vor. Dies ist natürlich nicht wünschenswert; idealerweise sollte es immer ein definiertes Verhalten geben, wenn ein bestimmtes Objekt von einem solchen Algorithmus bearbeitet wird.

Beide Probleme löst die Bildung von abstrakten Klassen bzw. Schnittstellen-Klassen. Durch die Spezialisierung kann es zu einer Operation mehrere Methoden in der Ver­erbungshierarchie geben, wobei die jeweilige Methode zur Laufzeit von der tatsächli­chen Klasse des Objekts abhängt. Eine Erweiterung des Programms führt einfach zu dem Hinzufügen einer neuen Klasse, die ebenfalls die Schnittstellen-Klasse imple­mentiert. Die Algorithmen müssen sich nicht ändern, das erste Problem ist gelöst.

Weiterhin stellt die Sprache C++ sicher, dass es niemals Objekte zu einer abstrakten Klasse bzw. einer Schnittstellen-Klasse gibt und dass ein Objekt zu jeder Operation in seiner vollständigen Schnittstelle über seine Klasse eine entsprechende Methode besitzt. Dies beseitigt somit das zweite Problem.

In unserem Beispiel wollen wir eine Funktion implementieren, die ein graphisches Objekt verschiebt, jedoch vorher das Objekt versteckt und hinterher wieder anzeigt. Diese Funktion wollen wir safeMove nennen. Da diese Funktion auf allen graphi­schen Objekten arbeiten soll, hat sie als Parameter eine Referenz auf ein Objekt vom Typ GraphicalObject. Damit wird genau die gewünschte Eigenschaft von sa­feMove ausgedrückt: die Möglichkeit, beliebige Objekte zu verarbeiten, sofern sie die Schnittstelle GraphicalObject implementieren.

117 // verschiebt ein Objekt um deltaX Pixel nach rechts und deltaY Pixel nach unten118 // negative Werte ändern die Richtung der Verschiebung119 // versteckt das Objekt vorher und zeigt es hinterher wieder an120 void safeMove (GraphicalObject &object, int deltaX, int deltaY)121 {122 object.hide ();123 object.move (deltaX, deltaY);124 object.show ();125 }126

Unsere main-Funktion ruft nun zusätzlich safeMove beispielhaft auf, um zu ver­deutlichen, dass diese Funktion sowohl mit Punkten als auch mit Linien umgehen kann:

127 int main ()128 {129 Point p1 (1, 2);130 Point p2 (3, 5);131 Line l (p1, p2);132 l.show ();133134 Point p3 (6, 8);135 p3.show ();136137 safeMove (l, 2, 3); // Aufruf mit einer Linie138 safeMove (p3, 3, 4); // Aufruf mit einem Punkt139140 return 0;

169

Funktion operiert auf Objekten mit GraphicalObject-Schnittstelle

Nicht-Verwenden von Abstraktio­nen kann zu Feh­lern führen

Generalisierung und Spezialisie­rung sind die Lö­sung

Page 178: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

141 }

Die Ausgabe ist:Showing line: Point (1, 2) shown. Point (3, 5) shown.Line shown.Point (6, 8) shown.Hiding line: Point (1, 2) hidden. Point (3, 5) hidden.Line hidden.Showing line: Point (3, 5) shown. Point (5, 8) shown.Line shown.Point (6, 8) hidden.Point (9, 12) shown.

Kommen wir zum Namen dieses Abschnitts. Polymorphie kommt aus dem Griechi­schen und bedeutet Vielgestaltigkeit. Was hat dies nun mit dem obigen Programm zu tun? Nun, wenn Sie sich den Parameter object in Zeile 120 anschauen:

120 void safeMove (GraphicalObject &object, int deltaX, int deltaY)werden Sie feststellen, dass Sie innerhalb der Funktion gar nicht wissen, von welcher Klasse object wirklich ist. Gewiss, Sie wissen, dass es die Schnittstelle Graphi­calObject implementiert, aber das sagt nichts über dessen wirkliche Klasse aus. In unserem Beispiel implementieren sowohl Line als auch Point diese Schnittstel­le, somit können sich hinter object sowohl Line- als auch Point-Objekte ver­bergen. Folglich ist object polymorph oder vielgestaltig: Zu verschiedenen Zeit­punkten kann dieser Parameter mal eine Referenz auf ein Point-Objekt und mal eine Referenz auf ein Line-Objekt sein. Und später, wenn das Programm um weite­re graphische Objekte erweitert wird, funktioniert der Algorithmus auch mit Ellipsen, Kreisen, Polygonen u. s. w., ohne dass auch nur eine einzige Zeile Code geändert werden muss! Dieser enorme Vorteil begründet den folgenden Merksatz:

Merksatz 23: Verwende Polymorphie anstatt Fallunterscheidungen!

Damit Polymorphie auch funktioniert, müssen Referenzen oder Zeiger verwendet werden. Warum? Weil Sie ansonsten immer ein konkretes Objekt „haben“ und des­sen Klasse von vornherein kennen. Über die Referenz bzw. den Zeiger erreichen Sie die nötige Indirektion: Sie betrachten sozusagen das Objekt aus einer gewissen Ent­fernung. Diese Entfernung ist gerade weit genug, um die Unterschiede zwischen den konkreten Klassen (Line oder Point) verwischen zu lassen; sie ist aber auch nah genug, um mit den Objekten etwas anfangen zu können (Operationen der Schnittstel­le GraphicalObject).

Wenn Sie im obigen Fall keine Referenz verwenden:120 void safeMove121 (GraphicalObject object, int deltaX, int deltaY)

170

Polymorphie ist Vielgestaltigkeit

Polymorphie er­fordert Verweise

Page 179: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

wird der Übersetzer einen Fehler melden. Sie versuchen dann nämlich, ein Objekt der Klasse GraphicalObject zu erzeugen. Sie erinnern sich, dass ein Argument bei normaler Wert-Übergabe in den entsprechenden Parameter kopiert wird (4.5.3.1). Dabei ist die Kopie vom Typ des Ziel-Objekts. Das Ziel-Objekt ist hier aber ein Parameter vom Typ GraphicalObject. Da ein Objekt zu dieser Klasse nicht erstellt werden kann, weil sie abstrakt ist, liegt ein Fehler vor.

4.6.4 EinfachvererbungWir haben in Abschnitt 4.6.2 gesehen, dass die Syntax für das Spezialisieren von Schnittstellen

class Unterklasse : public Oberklasse

lautet. Dabei handelt es sich um Einfachvererbung, weil es eine Beziehung zu genau einer Oberklasse ist. Dieser Abschnitt beschränkt sich auf eben diesen Fall; der nächste Abschnitt behandelt den allgemeinen Fall, nämlich dass eine Unterklasse von mehr als einer Oberklasse erbt.

Je nachdem, was die Unterklasse in Bezug auf die Oberklasse „tut“, spricht man von verschiedenen Dingen. Die Möglichkeiten sind:

(1) Erweiterung: Die Unterklasse fügt neue Operationen hinzu.

(2) Spezialisierung ohne Redefinition: Wie (1); zusätzlich fügt die Unterklasse neue Attribute und Methoden hinzu.

(3) Spezialisierung mit Redefinition: Wie (2); zusätzlich redefiniert die Unterklas­se Methoden der Oberklasse.

Die obige Syntax kann alle drei Vorgänge ausdrücken. Beispiel:1 // Oberklasse für die folgenden Beispiele2 class GraphicalObject3 {4 public :5 virtual void move (int deltaX, int deltaY) = 0;6 };78 // #1: Erweiterung9 class DisplayableGraphicalObject : public GraphicalObject10 {11 public :12 virtual void show () = 0;13 virtual void hide () = 0;14 };1516 // #2: Spezialisierung ohne Redefinition:17 class Point : public DisplayableGraphicalObject18 {19 public :20 Point (int x, int y);21 virtual void show ();22 virtual void hide ();23 virtual void move (int deltaX, int deltaY);24 private :25 int m_x;26 int m_y;27 };

171

Bedeutung der C++-Spezialisie­rung

Wiederholung: Spielarten der Spezialisierung

Page 180: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

2829 // #3: Spezialisierung mit Redefinition:30 class DebugPoint : public Point31 {32 public :33 DebugPoint (int x, int y);34 virtual void show ();35 virtual void hide ();36 virtual void move (int deltaX, int deltaY);37 };38

Das entsprechende UML-Klassendiagramm können Sie in Abbildung 39 finden. Wie Sie sehen, unterscheidet UML nicht zwischen einer Schnittstellen-Erweiterung (zwi­schen DisplayableGraphicalObject und GraphicalObject) und ge­wöhnlicher Vererbung (zwischen DebugPoint und Point).

Die ersten beiden Verwendungsweisen haben wir bereits ausführlich besprochen so­wie deren Einsatz gezeigt. Im Rest dieses Abschnitts wollen wir uns mit der letzten Variante, der Spezialisierung mit Redefinition, befassen.

4.6.4.1 Redefinition einer MethodeWie Sie an dem letzten Beispiel gesehen haben, funktioniert die Redefinition einer Methode ähnlich der Implementierung einer Operation: Die Methode ist sowohl in der Oberklasse (hier: Point) als auch in der Unterklasse (hier: DebugPoint) als virtual definiert.43 Beide Klassen sind konkret, also besitzen beide zu allen Ope­rationen eine entsprechende Methode. Natürlich wird die Methode der Klasse 43) Genauer gesagt ist es nur erforderlich, dass die Definition in der Oberklasse mit dem Schlüssel­

wort virtual versehen wird; alle Redefinitionen sind damit automatisch virtual.

172

Redefinition einer Methode

Abbildung 39: Verschiedene Spielarten der Vererbung

<<interface>>GraphicalObject

void move (int deltaX, int deltaY)

<<interface>>DisplayableGraphicalObject

void show ()void hide ()

Point

Point (int x, int y)void show ()void hide ()void move (int deltaX, int deltaY)

int m_xint m_y

<<realize>>

DebugPoint

DebugPoint (int x, int y)void show ()void hide ()void move (int deltaX, int deltaY)

Page 181: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

Point verwendet, wenn ein Point-Objekt vorliegt, und die Methode der Klasse DebugPoint, wenn ein DebugPoint-Objekt vorliegt. Dies funktioniert nach den Prinzipien der Polymorphie (4.6.3) auch für den Zugriff über eine Referenz oder ei­nen Zeiger auf die Oberklasse. Beispiel:

1 int main ()2 {3 Point point;4 DebugPoint debugPoint;56 point.show (); // führt Point::show aus7 debugPoint.show (); // führt DebugPoint::show aus89 Point &pointRef = debugPoint;10 pointRef.show (); // führt DebugPoint::show aus11 // weil pointRef auf ein DebugPoint-Objekt verweist1213 return 0;14 }

Wenn Sie keine Referenz oder keinen Zeiger verwenden, funktioniert die Polymorphie nicht, weil Sie ein neues Objekt der entsprechenden Oberklasse erzeugen. Falls die Oberklasse eine konkrete Klasse ist (wie Point im obigen Beispiel), kann der Überset­zer Ihren Fehler nicht erkennen, und er erzeugt ein entsprechendes, neues Objekt, indem er das Objekt der Unterklasse entsprechend abschneidet. Beispiel:

1 Point point2 = debugPoint;2 point2.show(); // führt Point::show aus! point2 ist ein Point-Objekt!

Hier ist point2 ein reguläres Objekt der Klasse Point, dessen Attribut-Werte durch die entsprechenden Werte des Objekts debugPoint belegt worden sind. Durch die Zuweisung wird hier also eine Kopie des Point-Anteils erzeugt; somit ist verständlich, dass die DebugPoint-Methode show nicht ausgeführt wird.

Dieses Erzeugen eines „abgeschnittenen“ Objekts wird in der Fachliteratur als Slicing44 bezeichnet. Es geschieht immer bei einer Zuweisung oder Initialisierung, wenn das Quell-Objekt zu einer konkreteren und das Ziel-Objekt zu einer generelleren Klasse ge­hört. In manchen Fällen ist ein solches Abschneiden nicht möglich, etwa wenn der Ziel-Typ der Zuweisung oder Initialisierung ein abstrakter Typ ist. (Dies war z. B. in der Funktion safeMove der Fall, wo der Parameter vom Typ „Referenz auf Graphi­calObject“, einer (abstrakten) Schnittstellen-Klasse, war.)

Es stellen sich nun zwei Fragen, die wir im Folgenden untersuchen wollen:

(1) Wann ist eine Redefinition syntaktisch korrekt?

(2) Wann ist eine Redefinition semantisch korrekt?

4.6.4.1.1 Syntaktisch korrekte Redefinitionen

Zum ersten Punkt: Es ist erforderlich, dass die redefinierende Methode genauso aus­sieht wie die redefinierte. Das bedeutet konkret:

• Der Name muss identisch sein.

• Der Rückgabetyp muss identisch sein (hier gibt es eine mögliche Ausnahme, sie­he hierzu den nächsten eingerückten Absatz).

44) engl. to slice = (ab)schneiden

173

Zuweisungen von konkreten Objek­ten und Slicing

Achten Sie auf Verweise!

Wann ist eine Re­definition syntak­tisch korrekt?

Page 182: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

• Die Parameter müssen in Anzahl, Reihenfolge und Typ übereinstimmen. Die Pa­rameter-Namen können sich jedoch unterscheiden.

• Die Qualifizierung der Methode (const oder nicht const) muss identisch sein.

Ansonsten laufen Sie Gefahr, eine neue Methode zu definieren, ohne die bestehende zu redefinieren! Ein Beispiel soll dies verdeutlichen:

1 class A2 {3 public :4 virtual int square (int i);5 };6 class B : public A7 {8 public :9 virtual long square (long l);

10 };

Hier wird die Methode A::square nicht in der Klasse B redefiniert. Vielmehr wird eine zusätzliche Methode eingeführt, die mit derjenigen in A nur den Namen gemein hat und diese im Kontext von B sogar verdeckt. Insbesondere funktioniert – weil es sich hier nicht um eine Redefinition handelt – Polymorphie nicht korrekt: Wenn Sie ein B-Objekt über einen A-Verweis nutzen und die Operation square in Anspruch nehmen, wird immer die Methode A::square ausgeführt!

Leider wird das obige Beispiel nicht vom Übersetzer zurückgewiesen, weil er in an­deren Situationen (in Verbindung mit Überladung (7.1) und sog. using-Deklaratio­nen) durchaus Sinn machen kann. Ein wirklicher Fehler tritt nur dann auf, wenn die redefinierende Methode und die redefinierte Methode identisch sind bis auf den Rückgabetyp. Da C++ zwei Operationen nicht ausschließlich am Rückgabetyp unter­scheiden kann (s. Abschnitt 7.1), liegt dann ein Fehler vor. Moderne Übersetzer ge­ben aber in allen anderen (legalen, aber fraglichen) Fällen, in denen eine virtuelle Methode von einer anderen nicht redefiniert, sondern verdeckt wird, eine Warnung aus.

Merksatz 24: Achte bei der Redefinition einer Methode auf Typ-Gleichheit!

Die obige Bemerkung, dass die Rückgabetypen der redefinierenden und der redefinier­ten Methode identisch sein müssen, ist nicht hundertprozentig korrekt: Der Rückgabe­typ der redefinierenden Methode darf auch spezieller sein als derjenige der redefinierten Methode. Man spricht in diesem Fall von Kovarianz bzw. von Methoden mit kovarian­ten Rückgabetypen. Beispiel:

1 /*** Beispiel kovarianz.cpp ***/2 // Schnittstelle für alle Objekte, die kopiert werden können3 class Cloneable4 {5 public :6 virtual Cloneable *Clone () const = 0;7 };8 // kapselt Zahlen9 class Number : public Cloneable

10 {11 public :12 Number (int i);

174

Probleme bei in­korrekter Redefi­nition

Kovarianz

Page 183: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

13 int Value () const;14 virtual Number *Clone () const;15 private :16 int m_i;17 };18 Number::Number (int i) : m_i (i) {}19 int Number::Value () const {return m_i;}20 Number *Number::Clone () const21 {return new Number(*this);}2223 void genericClone(Cloneable &c)24 {25 Cloneable *copy26 = c.Clone (); // Typ des Ausdrucks ist „Cloneable*“27 delete copy;28 }2930 int main ()31 {32 Number n1 (33);33 genericClone (n1);34 Number *n235 = n1.Clone (); // Typ des Ausdrucks n1.Clone() ist „Number*“36 delete n2;37 return 0;38 }

Der Rückgabetyp der Operation Number::Clone() ist kovariant: In der Basisklasse Cloneable ist er Cloneable*, in der abgeleiteten Klasse Number ist er Num­ber*. Das ist nach der oben genannten Regel jedoch in Ordnung, da Number* spezi­eller ist als Cloneable* (jeder Zeiger auf ein Number-Objekt kann einem Zeiger auf ein Cloneable-Objekt zugewiesen werden, s. Abschnitt 3.4.5). Die Kovarianz macht hier Sinn, weil der Ausdruck in Zeile 35 ansonsten vom Typ Cloneable* wäre und die Initialisierung des Number-Zeigers n2 mit einem Cloneable-Zeiger ohne Cast (3.4.5.2) zu einem Fehler führte. Deshalb verhindert hier der speziellere Rückgabetyp eine (überflüssige) explizite Typ-Umwandlung.

Leider unterstützt VC++ keine kovarianten Rückgabetypen! Hier müssen Sie also auf explizite Typ-Umwandlungen zurückgreifen.

Merksatz 25: Nutze kovariante Rückgabetypen, um Casts zu vermeiden!

4.6.4.1.2 Semantisch korrekte Redefinitionen

Zum zweiten Punkt: Eine Methode ist genau dann eine semantisch korrekte Redefini­tion einer anderen Methode, wenn das Liskov’sche Substitutionsprinzip (4.1.6) wei­terhin volle Gültigkeit hat. Dies bedeutet im Kontext von Methoden:

(1) Eine redefinierende Methode darf nicht mehr verlangen als die redefinierte Me­thode. Das heißt, dass Vorbedingungen gelockert, aber nicht verschärft werden dürfen. Ein typisches Beispiel für unzulässig verschärfte Vorbedingungen ist die Einschränkung des Wertebereichs eines oder mehrerer Parameter.

(2) Eine redefinierende Methode darf nicht weniger versprechen als die redefinierte Methode. Das heißt, dass Nachbedingungen verschärft, aber nicht gelockert wer­den dürfen. Ein typisches Beispiel für unzulässig gelockerte Nachbedingungen ist die Erweiterung des Wertebereichs des zurückgegebenen Wertes.

175

Wann ist eine Re­definition seman­tisch korrekt?

Vorbedingungen dürfen gelockert werden

Nachbedingungen dürfen verschärft werden

Page 184: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

Wieso tragen diese Regeln dazu bei, dass das Substitutionsprinzip gilt? Wir wollen dies an zwei kleinen Beispielen veranschaulichen:

1 // Ausnahme-Klasse für ungültige Argumente (s. Abschnitt 5.2)2 class UngueltigesArgument {};34 // Fakultät-Dienst5 class Fakultaet6 {7 public :8 // Berechnet die Fakultät von i. i muss positiv sein, ansonsten wird eine9 // Ausnahme vom Typ UngueltigesArgument ausgeworfen (siehe Abschnitt 5.2).

10 virtual int berechne (int i);11 };1213 class MeineFakultaet : public Fakultaet14 {15 public :16 virtual int berechne (int i); // Redefinition17 };18

Die Methode Fakultaet::berechne berechnet die Fakultät des Parameters i. Sie hat die Vorbedingung, dass i positiv ist, und die Nachbedingung, dass die Fakul­tät des Parameters berechnet wird.

Nun schauen wir uns ein paar mögliche Reimplementierungen von berechne an:

(1) Die erste Redefinition erlaubt alle Zahlen (inklusive negativer Zahlen) als Argu­mente.

(2) Die zweite Redefinition erlaubt nur die Zahlen von 0 bis 10 als Argumente, an­sonsten wird eine Ausnahme vom Typ UngueltigesArgument ausgewor­fen.

(3) Die dritte Redefinition ist für alle positiven Zahlen genauso definiert wie die ur­sprüngliche Methode, bei negativen Zahlen hingegen gibt sie Null zurück, anstatt eine UngueltigesArgument-Ausnahme zu erzeugen.

(4) Die vierte Redefinition liefert immer Eins zurück.

Zur ersten Redefinition: Die erste Regel besagt, dass diese Redefinition korrekt ist, weil die Vorbedingungen nicht mehr verlangt als die ursprüngliche Methode. Und in der Tat, es gibt kein Programm, das mit der ursprünglichen Methoden-Definition funktioniert und mit der neuen nicht. Dies sieht man leicht, indem wir die möglichen Argumente überprüfen: Die ursprüngliche Methode war nur für positive Argumente definiert; die neue verhält sich aber in diesem Bereich genauso wie die alte. Das Ver­halten hat sich nur bei negativen Argumenten geändert; da diese jedoch von der Vor­bedingung der ursprünglichen Methode ausgeschlossen waren, führt die Redefinition zwar neue Funktionalität hinzu, ändert aber nichts an korrekten alten Programmen, die gegen die Spezifikation der alten Methode implementiert wurden. Diese Redefi­nition hat also die Vorbedingungen gelockert, was in Ordnung ist.

Die zweite Redefinition hingegen ist hochgradig problematisch. Durch die Ein­schränkung eines Eingabe-Parameters haben wir effektiv die Vorbedingung der Me­

176

korrekte und in­korrekte Redefini­tionen

Page 185: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

thode verschärft, weil nun wesentlich weniger Werte an die Methode übergeben wer­den dürfen. Die erste Regel macht uns aber darauf aufmerksam, dass wir Vorbedin­gungen nicht verschärfen dürfen. Dass diesmal das Substitutionsprinzip wirklich ver­letzt ist, wird deutlich, wenn wir die neue Methode nutzen, um die Fakultät von 6 zu berechnen: Während die ursprüngliche Methode keine Probleme damit hatte, wirft die Redefinition eine Ausnahme aus. Somit hat sich das Verhalten des Programms durch die Substitution geändert.

Die dritte Redefinition ist korrekt, obwohl das manchen vielleicht erstaunen mag. Ähnlich wie bei der ersten Redefinition gilt hier, dass existierende Vorbedingungen nicht verschärft werden, sondern das Verhalten sich nur für Werte ändert, die vorher tabu waren. Die Vorbedingungen werden gelockert, weil jetzt vorher undefinierte Funktionsergebnisse nun „definierter“ werden.

Die vierte Redefinition ist natürlich inkorrekt. Dies liegt daran, dass die ursprüngli­chen Nachbedingungen nicht mehr erfüllt werden: Während die originäre Methode zusagt, für positive Zahlen die Fakultät zu berechnen, kann das die Redefinition – au­ßer in den Fällen i = 0 und i = 1 – nicht mehr garantieren. Die Nachbedingungen sind also gelockert worden, was von der zweiten Regel explizit verboten wird.

Wir schauen uns jetzt ein zweites Beispiel an, um zu zeigen, dass verschärfte Nach­bedingungen das Substitutionsprinzip nicht verletzen:

1 // Stellt einen instabilen Sortieralgorithmus dar.2 class InstableSorter3 {4 public :5 // sortiert die Elemente in dem Container;6 // stellt nicht sicher, dass gleiche Elemente ihre relative Ordnung beibehalten7 virtual void sort (Container &c);8 };910 // Stellt einen stabilen Sortieralgorithmus dar.11 class StableSorter : public InstableSorter12 {13 public :14 // sortiert die Elemente in dem Container;15 // stellt sicher, dass gleiche Elemente ihre relative Ordnung beibehalten16 virtual void sort (Container &c);17 };

Die Idee der Klasse InstableSorter ist es, einen Sortieralgorithmus zur Verfü­gung zu stellen. Die Nachbedingung der Methode sort ist klar: Die Elemente des Containers sind nach der Anwendung der Methode sortiert. Die Vorbedingungen sind weniger klar, allerdings muss die Methode zwei Elemente vergleichen können, um eine Sortierung erfolgreich durchzuführen. Deshalb existiert die Vorbedingung, dass eine totale Ordnung auf den Elementen des Containers existiert.

Weiterhin ist der Algorithmus, der von dieser Klasse implementiert wird, instabil: Das bedeutet, dass die relative Reihenfolge gleicher Elemente zueinander bei einer Sortierung nicht beibehalten werden muss (aber kann). Ein Beispiel: Beinhaltet der Container Zeichenketten, und definieren wir, dass die Teil-Zeichenketten „ä“ und „ae“ bei der Sortierung gleich behandelt werden sollen, dann sind beispielsweise die

177

stabile und insta­bile Sortieralgo­rithmen

Page 186: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

Wörter „Säge“ und „Saege“ gleich. Haben wir nun einen Container, in dem diese Wörter vorkommen (Abbildung 40), so darf ein instabiler Sortieralgorithmus diese Wörter untereinander vertauschen (Abbildung 41), ein stabiler hingegen nicht (Ab­bildung 42).

Die Klasse StableSorter implementiert nun einen anderen Sortieralgorithmus, der verspricht, dass der Sortiervorgang stabil ist. Ein stabiler Algorithmus garantiert alles, was ein instabiler Sortieralgorithmus auch garantiert, und darüber hinaus noch mehr: Er garantiert, dass gleiche Elemente nicht in ihrer relativen Reihenfolge ver­tauscht werden. Die Ersetzung eines – möglicherweise – instabilen Sortieralgorith­mus durch einen stabilen Sortieralgorithmus ist eine korrekte Redefinition gemäß der zweiten Regel, denn alle Klienten des alten Sortieralgorithmus können mit dem neu­en arbeiten, ohne dass sich für sie etwas ändert. Das kommt daher, dass Klienten bis­her nicht von einer bestimmten Reihenfolge gleicher Elemente untereinander ausge­hen konnten – schließlich war der Algorithmus, den sie benutzten, instabil. Somit gilt das Substitutionsprinzip, und die Welt ist in Ordnung.

Anders sieht es aus, wenn die Klassenhierarchie umgedreht wird: Die Basisklasse stellt in diesem Fall den stabilen und die abgeleitete Klasse den instabilen Algorith­mus dar. Nun hat ein Klient bei der Nutzung der ursprünglichen Implementierung bisher davon ausgehen können, dass der Algorithmus die relative Reihenfolge glei­cher Elemente beibehält. Wird nun dieser Algorithmus durch einen instabilen reim­plementiert, ist dies nicht mehr gewährleistet, und der Klient hat es auf einmal mit ei­nem – aus seiner Sicht – nicht richtig sortierten Container zu tun, was zu allerhand Fehlern führen kann. Hieran sehen Sie, dass das Lockern von Nachbedingungen falsch ist und zu inkorrektem Programmverhalten führen kann.

Leider kann Ihr Übersetzer semantische Verletzungen des Substitutionsprinzips nicht erkennen und Sie folglich nicht darauf aufmerksam machen. Deshalb müssen Sie sol­che Überlegungen bei jeder Redefinition einer Methode selbst anstellen und sicher­stellen, dass das Substitutionsprinzip gewahrt bleibt.

178

Abbildung 40: Ursprüngliche Anordnung der Elemente

„Säge“„Saege“ „Hammer“„Schaufel“ „Hobel“

1 2 3 4 5

Abbildung 42: Anordnung nach stabiler Sortierung

„Säge“„Saege“„Hammer“ „Schaufel“„Hobel“

1 2 3 4 5

Abbildung 41: Mögliche Anordnung nach instabiler Sortierung

„Säge“ „Saege“„Hammer“ „Schaufel“„Hobel“

1 2 3 4 5

semantische Ver­letzungen werden vom Übersetzer nicht gefunden

Page 187: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

Eine letzte Bemerkung noch: Dieselben Überlegungen gelten auch für (abstrakte) Operationen. Hier existiert bloß kein Code, so dass man eine eventuelle Verletzung des Substitutionsprinzips nicht so einfach demonstrieren kann. Die Überlegungen zu Vor- und Nachbedingungen sind jedoch genauso zu führen wie bei implementierten Operationen.

4.6.4.2 Aufruf der OberklasseRedefinierende Methoden in der Unterklasse können die redefinierten Methoden der Oberklasse aufrufen. Dies ist häufig notwendig, um das erwartete Verhalten der Ope­ration aus der Sicht der Klienten sicherzustellen (4.6.6). Dabei kann eine geeignete Vor- und Nacharbeitung sinnvoll sein. Beispiel:

1 void Point::show ()2 {3 cout << "(" << m_x << ", " << m_y << ")" << endl;4 }5 void DebugPoint::show ()6 {7 cout << "DebugPoint: in Methode show (Anfang)" << endl;8 Point::show ();9 cout << "DebugPoint: in Methode show (Ende)" << endl;10 }

Hier involviert die redefinierende Methode DebugPoint::show die redefinierte Methode in der Klasse Point auf, um die eigentliche Arbeit zu erledigen. Vorher und nachher tut sie jedoch noch andere Dinge; in unserem Fall gibt sie zusätzliche Informationen über die Ausführung der Methode aus.

Die Syntax beim Aufruf einer Methode der Oberklasse ist folglich:

Oberklasse :: Methode ( [ Argumente ] )Es ist nicht zwingend notwendig, in einem solchen Fall die Methode der Oberklasse aufzurufen. Allerdings muss man die Erwartungen seiner Klienten erfüllen (4.6.6), und da man die Funktionalität normalerweise nicht noch einmal implementieren möchte, wird man in vielen Fällen auf die Dienste der vorhandenen Methode in der Oberklasse zurückgreifen.

Sie können mit Hilfe dieser Syntax keine Konstruktoren und Destruktoren der Oberklas­se aufrufen. Der Konstruktor der Oberklasse kann nur im Konstruktor der Unterklasse aufgerufen werden (4.6.4.4), der Destruktor der Oberklasse wird immer nur implizit vom Destruktor der Unterklasse aufgerufen (4.6.4.5).

4.6.4.3 Vererbung und PolymorphieSie haben sicherlich gemerkt, dass in diesem Abschnitt alle Methoden-Deklarationen mit dem Schlüsselwort virtual versehen wurden. Das hat seine Berechtigung: Nur so funktionieren Vererbung und Polymorphie reibungslos miteinander. Betrachten Sie dazu das folgende Beispiel:

1 /*** Beispiel virtual.cpp ***/2 #include <ostream>3 #include <iostream>4 #include <string>5 using namespace std;

179

Arbeitsteilung zwischen Unter­klasse und Ober­klasse

Syntax bei Dele­gation an Ober­klasse

die Bedeutung von virtual

Substitutionsprin­zip gilt auch für Implementierung von Operationen

Page 188: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

67 // einfache Ausgabe-Klasse8 class VirtualPrinter9 {

10 public :11 // gibt "message" aus12 virtual void print (const string &message);13 };1415 // einfache Ausgabe-Klasse16 class NonVirtualPrinter17 {18 public :19 // gibt "message" aus20 void print (const string &message);21 };2223 void VirtualPrinter::print (const string &message)24 {25 cout << message << endl;26 }27 void NonVirtualPrinter::print (const string &message)28 {29 cout << message << endl;30 }31

Zuerst haben wir zwei Printer-Klassen definiert, die beide eine Operation print implementieren. Der einzige Unterschied zwischen diesen beiden Klassen ist – abge­sehen vom Namen – der virtual-Modifizierer bei der Operation: in Virtual­Printer ist er vorhanden, in NonVirtualPrinter nicht. Ansonsten ist die Im­plementierung der beiden Operationen identisch.

32 // erweitert VirtualPrinter um eine zusätzliche Ausgabe33 class DebugVirtualPrinter : public VirtualPrinter34 {35 public :36 // gibt "message" aus37 virtual void print (const string &message);38 };3940 // erweitert NonVirtualPrinter um eine zusätzliche Ausgabe41 class DebugNonVirtualPrinter : public NonVirtualPrinter42 {43 public :44 // gibt "message" aus45 void print (const string &message);46 };4748 void DebugVirtualPrinter::print (const string &message)49 {50 cout << "VirtualPrinter::print(" << message << ")" << endl;51 VirtualPrinter::print (message);52 }53 void DebugNonVirtualPrinter::print (const string &message)54 {55 cout << "NonVirtualPrinter::print(" << message << ")" <<

endl;56 NonVirtualPrinter::print (message);57 }58

180

eine Klasse mit und eine ohne virtual-Ope­rationen

Page 189: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

Hier haben wir zwei weitere Klassen definiert, die jeweils von den vorherigen beiden erben. Diese Debug-Klassen erweitern die print-Methoden um eine zusätzliche Ausgabe. Diese zusätzliche Ausgabe erlaubt uns zu verstehen, was für eine Rolle der virtual-Modifizierer bei Operationen in Zusammenhang mit Vererbung und Poly­morphie spielt.

Schauen wir uns nun an, wie sich Objekte dieser Klassen bei Benutzung verhalten:59 int main ()60 {61 VirtualPrinter vp;62 NonVirtualPrinter nvp;63 DebugVirtualPrinter dvp;64 DebugNonVirtualPrinter dnvp;6566 // Nutzung der Nicht-Debug-Objekte67 // direkter Aufruf68 vp.print ("Hallo Virtual direkt");69 nvp.print ("Hallo NonVirtual direkt");70

Die direkte Benutzung der beiden Printer-Objekte liefert – wie erwartet – das er­wartete Ergebnis:

Hallo Virtual direktHallo NonVirtual direkt

Jetzt benutzen wir die Debug-Objekte:71 // Nutzung der Debug-Objekte72 // direkter Aufruf73 dvp.print ("Hallo DebugVirtual direkt");74 dnvp.print ("Hallo DebugNonVirtual direkt");75

Die Benutzung der Debug-Objekte führt zu den erwarteten Meldungen, es erfolgt je­doch vorher eine zusätzliche Ausgabe:

VirtualPrinter::print(Hallo DebugVirtual direkt)Hallo DebugVirtual direktNonVirtualPrinter::print(Hallo DebugNonVirtual direkt)Hallo DebugNonVirtual direkt

Bis jetzt funktioniert alles wie gehabt, und es ist kein Unterschied zwischen der Vari­ante mit und der ohne virtual-Schlüsselwort zu erkennen. Jetzt greifen wir auf die Objekte polymorph, d. h. über einen Verweis (hier: einen Zeiger) zu:

76 // Nutzung der Nicht-Debug-Objekte77 // Aufruf über Zeiger auf die Basisklasse78 VirtualPrinter *pvp = &vp;79 NonVirtualPrinter *pnvp = &nvp;80 pvp->print ("Hallo Virtual über Virtual-Zeiger");81 pnvp->print ("Hallo NonVirtual über NonVirtual-Zeiger");82

Hier wird auf die Printer-Objekte über entsprechende Zeiger zugegriffen. Da der Typ des Zeigers dem tatsächlichen Objekt entspricht, sind die Ergebnisse vorauszu­sehen: Es werden VirtualPrinter::print und NonVirtualPrinter::print aufgerufen, mit den folgenden Ausgaben als Ergebnis:

181

beide Klassen werden speziali­siert und die Me­thoden redefiniert

direkte Benutzung ergibt keinen Un­terschied

indirekter Zugriff führt zu unter­schiedlichen Er­gebnissen!

Page 190: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

Hallo Virtual über Virtual-ZeigerHallo NonVirtual über NonVirtual-Zeiger

Jetzt wird es interessant: Wir nutzen Polymorphie und greifen auf ein Objekt einer spezielleren Klasse über ein Zeiger auf eine allgemeinere Klasse zu:

83 // Nutzung der Debug-Objekte84 // Aufruf über Zeiger auf die Basisklasse85 pvp = &dvp;86 pnvp = &dnvp;87 pvp->print ("Hallo DebugVirtual über Virtual-Zeiger");88 pnvp->print ("Hallo DebugNonVirtual über NonVirtual-Zeiger");8990 return 0;91 }

Die Ergebnisse sind:VirtualPrinter::print(Hallo DebugVirtual über Virtual-Zeiger)Hallo DebugVirtual über Virtual-ZeigerHallo DebugNonVirtual über NonVirtual-Zeiger

Und hier kommt der Unterschied zwischen Methoden mit und ohne virtual-Mo­difizierer zum Tragen. Beim Zugriff auf das Objekt dnvp der Klasse DebugNon­VirtualPrinter über den Zeiger pnvp wird beim Senden der Nachricht print diese nicht durch die Methode DebugNonVirtualPrinter::print behandelt, sondern durch die Methode NonVirtualPrinter::print! Das Programm hat also „vergessen“, dass sich hinter dem Zeiger pnvp ein Objekt der Klasse De­bugNonVirtualPrinter verbirgt; vielmehr wird das Objekt so behandelt, als wäre es von der Klasse NonVirtualPrinter. Das widerspricht jedoch dem Grundgedanken von Polymorphie, dass über eine gemeinsame (und abstraktere) Schnittstelle (hier: NonVirtualPrinter) Objekte speziellerer Klassen (hier: Ob­jekt dnvp der Klasse DebugNonVirtualPrinter) verwendet werden können und sich diese dementsprechend verhalten.

Wir lernen daraus: Ist eine Klasse die Grundlage für speziellere Klassen, sollten alle Operationen virtual sein, damit Polymorphie funktioniert. Ist eine Klasse nicht dafür vorgesehen, dass sie spezialisiert wird, können Sie Operationen ohne das Schlüsselwort virtual deklarieren.

Es gibt nur sehr wenig Gründe dafür, eine Klasse nicht für weitere Spezialisierungen zu öffnen. In den meisten Fällen erweisen sich solche Gründe als Trugschlüsse, spätestens dann, wenn die Funktionalität dieser Klasse erweitert werden muss (z. B. auf Grund ei­nes Kundenwunsches). Sie sollten also nach Möglichkeit virtual-Operationen ver­wenden und somit ihre Klassen für Erweiterungen vorbereiten. Im Kapitel über Ent­wurfsmuster (6) werden Sie einige Muster kennen lernen, in denen auch die Definition und Nutzung von Methoden, die nicht virtual sind, Sinn macht (etwa das Template-Method-Muster (6.3.1)).

Jetzt verstehen Sie sicherlich auch, warum bei Schnittstellen die (abstrakten) Operatio­nen durch virtual und = 0 markiert werden. Durch virtual werden sie zur (mit Polymorphie verträglichen) Redefinition „freigegeben“, mit = 0 wird angezeigt, dass diese Klasse für diese Operationen keine Methode anbietet. Nur = 0 macht keinen Sinn, denn das würde bedeuten, dass die Methode zur Operation nicht existiert und auch durch speziellere Klassen nicht hinzugefügt werden kann.

182

virtual und Polymorphie ge­hen Hand in Hand

„Diese Klasse wird nie erwei­tert“ ist proble­matisch!

= 0 ohne vir­tual?

Page 191: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

4.6.4.4 Konstruktoren in VererbungshierarchienIn Abschnitt 4.6.2 wurde erwähnt, dass Konstruktoren und Destruktoren nicht vererbt werden, sich jedoch gegenseitig explizit oder implizit aufrufen. Genauer formuliert bedeutet das, dass innerhalb einer Vererbungslinie der Konstruktor einer jeden Klas­se aufgerufen werden muss, um „seinen“ Teil des Objekts entsprechend zu initialisie­ren.

Im obigen Beispiel muss also der Konstruktor der Klasse DebugPoint den Kon­struktor der Klasse Point aufrufen. Wäre er nicht dazu gezwungen, führte dies zu nicht initialisierten Attributen in der Oberklasse Point. Das ist generell nicht wün­schenswert. Konstruktoren von Oberklassen werden genauso wie Attribute in der In­itialisierungsliste eines jeden Konstruktors aufgerufen. Wir erweitern die Syntax der Initialisierungsliste also um den Aufruf von Konstruktoren:

:Element1 ( Argument(e) )[, Element2 ( Argument(e) )[, Element3 ( Argument(e) )[, ...]]]

In dieser Notation können Elemente sowohl Klassen-Namen als auch Attribut-Na­men sein. Im ersten Fall sind die Argumente die Konstruktor-Argumente für den Konstruktor der Oberklasse, wobei die Argument-Anzahl der Anzahl der Parameter im jeweiligen Konstruktor entspricht. Im zweiten Fall existieren ein oder mehrere Argument, die als Initialisierungsausdrücke für das jeweilige Attribut verwendet werden. (Mehrere Ausdrücke können natürlich nur dann verwendet werden, wenn das Attribut ein Objekt ist, dessen Klasse einen passenden Konstruktor besitzt.)

Wichtig ist festzustellen, dass bei der Initialisierung von Oberklassen nur die Kon­struktoren direkter Oberklassen aufgerufen werden dürfen. Konstruktoren indirekter Oberklassen (sofern vorhanden) werden von Konstruktoren ihrer (direkten) Unter­klasse aufgerufen. Jeder Konstruktor ist also nur für seine direkte Basisklasse (oder bei Mehrfachvererbung: seine direkten Basisklassen) verantwortlich.

Vielleicht verwirrt Sie, dass eine Klasse anscheinend mehr als eine Oberklasse haben kann. C++ unterstützt die sogenannte Mehrfachvererbung, bei der eine Klasse von mehr als einer Klasse erben kann. Genaueres erfahren Sie in Abschnitt 4.6.5.

Eine mögliche und sinnvolle Implementierung des DebugPoint-Konstruktors ist also beispielsweise:

1 DebugPoint::DebugPoint (int x, int y)2 :3 Point (x, y) // Aufruf des Konstruktors der Oberklasse4 {5 cout << "DebugPoint-Objekt erstellt" << endl;6 }

In Zeile 3 wird der Konstruktor der Oberklasse mit passenden Argumenten aufgeru­fen.

183

Initialisierungs­liste im Konstruk­tor (erweitert)

Konstruktor der Unterklasse ruft den der Oberklas­se auf

jeder Konstruktor ist nur für direkte Oberklassen ver­antwortlich

Page 192: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

Zur Reihenfolge der Initialisierung: Generell werden zuerst alle Oberklassen initiali­siert und dann die Attribute der eigenen Klasse. Dies ist auch logisch, wenn man den Konstruktionsprozess eines Objekts als „von unten nach oben“ oder „vom Abstrak­ten zum Konkreten“ betrachtet. Zur Reihenfolge der Initialisierung der einzelnen Oberklassen finden Sie in Abschnitt 4.6.5 nähere Informationen.

4.6.4.5 Destruktoren in VererbungshierarchienWie Sie in Abschnitt 4.5.2 erfahren haben, hat jede Klasse einen Destruktor (der durchaus vom Übersetzer automatisch generiert sein kann). Wenn diese Klasse eine Oberklasse hat, ruft der Destruktor nach getaner Arbeit den Destruktor seiner Ober­klasse auf. Es wird also immer der Destruktor der speziellsten Klasse als erstes aus­geführt. Dies entspricht der umgekehrten Reihenfolge der Konstruktor-Aufrufe: Dort wird zuerst der Konstruktor der allgemeinsten Klasse ausgeführt. Durch dieses Ver­halten wird sichergestellt, dass notwendige Aufräumungsarbeiten auch im Kontext von Vererbung ordnungsgemäß durchgeführt werden.

Aufpassen müssen Sie dennoch: Stellen Sie sicher, dass Ihre Destruktoren immer polymorph verwendet werden können, d. h. machen Sie Ihre Destruktoren virtu­al. Warum dies wichtig ist, zeigt folgendes Beispiel:

1 /*** Beispiel dtor.cpp ***/2 #include <ostream>3 #include <iostream>4 using namespace std;56 // normale Klasse mit virtuellem Destruktor7 class Base8 {9 public :

10 virtual ~Base ();11 };12 Base::~Base ()13 {14 cout << "in ~Base" << endl;15 }1617 // abgeleitete Klasse, ebenfalls mit virtuellem Destruktor18 class Derived : public Base19 {20 public :21 virtual ~Derived ();22 };23 Derived::~Derived ()24 {25 cout << "in ~Derived" << endl;26 }2728 int main ()29 {30 // Derived-Objekt erzeugen31 Base *base = new Derived;32 // Derived-Objekt über einen Zeiger auf Base zerstören → Polymorphie nutzen!33 delete base;34 return 0;35 }

184

Reihenfolge der Initialisierung

Machen Sie De­struktoren vir­tual!

Reihenfolge der Zerstörung

Page 193: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

Dieses Programm macht nichts weiter als zwei Klassen zu definieren (Base und Derived), ein Derived-Objekt zu erzeugen und es wieder zu zerstören, allerdings über einen Zeiger auf die Base-Klasse. Das Programm gibt ordnungsgemäß

in ~Derivedin ~Base

aus. (Denken Sie daran, dass der Destruktor der speziellsten Klasse zuerst aufgerufen wird.) Wenn Sie sich die Diskussion in Abschnitt 4.6.4.3 noch einmal vergegenwärti­gen, sehen Sie sofort das Problem, falls die Destruktoren nicht virtual sind: Da der Übersetzer in diesem Fall den tatsächlichen Typ des Objekts, das sich hinter dem base-Zeiger verbirgt, „vergisst“, wird nur der Destruktor der Klasse Base aufgeru­fen! Das bedeutet, dass in diesem Fall das Objekt nicht ordnungsgemäß zerstört wird. Die C++-Sprache sagt in einem solchen Fall, dass ein Fehler vorliegt und dass das Verhalten des Programms in solch einer Situation nicht vorhersehbar ist, sprich ein Programmabbruch oder Schlimmeres zur Folge haben kann. (Deshalb finden Sie das Beispiel mit nicht-virtuellen Destruktoren auch nicht im Skript, damit Ihre Festplatte durch das fehlerhafte Programm nicht aus Versehen formatiert wird...)

Solche Probleme treten natürlich nicht auf, wenn Sie auf die Objekte niemals poly­morph, d. h. über einen Zeiger oder eine Referenz auf eine Basisklasse, zugreifen. Doch wird in solchen Fällen üblicherweise von vornherein keine Vererbungsbezie­hung zwischen den Klassen existieren, da dies nur im Kontext von Polymorphie Sinn macht. Deshalb sollten Sie einer Schnittstellen-Klasse oder abstrakten Klasse immer einen virtuellen Destruktor spendieren (auch wenn er keine Anweisungen enthält).

Merksatz 26: Verwende virtuelle Destruktoren bei Basisklassen!

4.6.5 MehrfachvererbungMehrfachvererbung (oder Mehrfach-Spezialisierung) erlaubt Ihnen, von mehr als ei­ner Klasse zu erben bzw. mehr als eine Klasse zu spezialisieren. Die Syntax hierfür ist eine Erweiterung derer für das Spezialisieren einer Klasse:

: public Klassenname1, public Klassenname2 [, ...]

Die Mehrfachspezialisierung drückt aus, dass das Liskov’sche Substitutionsprinzip (4.1.6) für alle angegebenen Klassen Gültigkeit hat. Objekte der Klasse SIND bzw. VERHALTEN SICH also WIE Objekte der Klasse Klassenname1 und Objekte der Klasse Klassenname2. Das bedeutet, dass Objekte der Klasse alle Nachrichten ver­stehen, die auch Objekte der Klasse Klassenname1 und Objekte der Klasse Klassen­name2 verstehen.

Betrachten wir ein Beispiel zum besseren Verständnis. Wir wollen eine große objekt­orientierte Anwendung entwickeln, in denen viele Objekte vorkommen, die unter­schiedliche Schnittstellen besitzen und sich unterschiedlich verhalten. Nichtsdesto­weniger gibt es Gemeinsamkeiten. Insbesondere haben wir herausgefunden, dass wir viele Objekte haben, die wir an der Oberfläche anzeigen lassen wollen, etwa ein Sys­tem-Objekt, das Informationen über das Laufzeit-System der Anwendung (Betriebs­system, verwendete Bibliotheken etc.) anzeigt. Weiterhin werden an den unterschied­

185

nicht-virtuelle Destruktoren kön­nen zu Fehlern führen

Syntax für Mehr­fachvererbung

Motivation für Mehrfachspeziali­sierung

Page 194: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

lichsten Stellen in der Anwendung Objekte kopiert, etwa Objekte, die Personen-Da­ten (Name, Anschrift, Beruf etc.) kapseln.

Zwei Abstraktionen haben wir also identifiziert; nun wollen wir diese in C++ umset­zen. Wir wollen dafür zwei abstrakte Klassen einführen: eine für darstellbare Objekte (Displayable) und eine für kopierbare („klonbare“) Objekte (Cloneable).

1 /*** Beispiel multi.cpp ***/2 #include <ostream>3 #include <iostream>4 #include <string>5 using namespace std;67 // alle Objekte, die kopierbar sind, implementieren diese Schnittstelle8 class Cloneable9 {

10 public :11 virtual Cloneable *Clone () const = 0;12 };1314 // alle Objekte, die sich anzeigen können, implementieren diese Schnittstelle15 class Displayable16 {17 public :18 virtual void Display () const = 0;19 };20

(Wir haben hier die meisten Kommentare aus didaktischen Gründen weggelassen, weil wir uns auf das Wesentliche konzentrieren wollen. Machen Sie das ja nicht in Ihren eigenen Programmen! Das ist so ein typischer Punkt, an dem Lehrbücher von ihren eigenen Regeln abweichen (müssen)...)

So weit, so gut. Nun gibt es sicherlich Objekte, die darstellbar sind, aber nicht ko­piert werden sollen, etwa das oben erwähnte System-Objekt, das sicherlich nur ein­mal im ganzen System vorhanden sein soll (siehe hierzu auch die Beschreibung des Singleton-Musters in Abschnitt 6.4.2). Andersherum gibt es auch Objekte, die sich kopieren lassen, aber nicht unbedingt eine sinnvolle Repräsentation auf der Oberflä­che besitzen. Ein Beispiel hierfür sind Objekte, welche den Zugriff auf andere Ob­jekte kapseln, sog. Proxys (6.2.3). Manchmal macht es Sinn, die Zugriffe auf Objekte nicht direkt zu erlauben, sondern über andere Objekte zu regeln. Dadurch kann z. B. erreicht werden, dass gewisse Programm-Teile nur einen lesenden Zugriff auf ein Objekt gestattet bekommen und andere auch einen schreibenden Zugriff. Unter Um­ständen müssen solche Proxys (also Zugangskanäle) kopiert werden, um einem ande­ren Programmteil Zugang zu einem Objekt zu gewähren; allerdings will man solche internen „Hilfs“-Objekte natürlich nicht auf der Programm-Oberfläche darstellen.

Diese ganze Diskussion dient nur dem Zweck, um Ihnen zu verdeutlichen, dass die Abstraktionen Displayable und Cloneable nicht in einer Spezialisierungsbe­ziehung stehen: Weder ist Displayable spezieller als Cloneable noch umge­kehrt, weil es Objekte gibt, die jeweils nur eine Schnittstelle unterstützen können.

Nun hat unsere zu entwickelnde Anwendung aber auch Objekte, die sowohl auf der Oberfläche darzustellen als auch kopierbar sind. Ein typisches Beispiel wäre ein

186

Page 195: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

fachliches Objekt, das auch Daten kapselt, etwa ein Personen-Objekt. Personen-Da­ten muss man selbstverständlich anzeigen können, und man wird sicherlich Perso­nen-Objekte kopieren müssen, etwa bevor man dem Anwender eine Änderung am Objekt erlaubt (damit der Anwender immer noch die Möglichkeit hat, den ganzen Vorgang abzubrechen und die Daten so zu belassen, wie sie sind). Unsere Klasse Person versteht also beide Schnittstellen; folglich nutzen wir die Fähigkeit der Mehrfachspezialisierung, um dies in C++ abzubilden:

21 // eine Person kann kopiert und angezeigt werden22 class Person : public Cloneable, public Displayable23 {24 public :25 Person (const string &name);26 virtual Person *Clone () const;27 virtual void Display () const;28 private :29 string m_name;30 };3132 Person::Person (const string &name)33 : m_name (name)34 {35 }36 Person *Person::Clone () const37 {38 cout << "Kopiere Person " << m_name << endl;39 return new Person (*this);40 }41 void Person::Display () const42 {43 cout << "Person mit Namen " << m_name << endl;44 }45

Unsere Klasse Person implementiert nun beide Schnittstellen, sowohl Displaya­ble als auch Cloneable. Nun kann jeder Code, der über eine der beiden Schnitt­stellen auf ein Objekt zugreift, mit Person-Objekten umgehen:

46 // zeigt ein Displayable-Objekt an47 void Display (const Displayable &d)48 {49 d.Display ();50 }51 // fertigt eine Kopie eines Cloneable-Objekts an52 Cloneable *Clone (const Cloneable &n)53 {54 return n.Clone ();55 }56 int main ()57 {58 // erzeuge ein Person-Objekt59 Person p ("Markus");60 // zeige die Person an61 Display (p);62 // klone die Person (natürlich nur das Objekt!)63 Cloneable *c = Clone (p);64 // lösche Kopie65 delete c;66 return 0;67 }

187

Page 196: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

Die Funktionen in den Zeilen 46-50 und 51-55 greifen jeweils auf die Display­able- bzw. Cloneable-Schnittstelle eines Objekts zu und zeigen es an bzw. ferti­gen eine Kopie des Objekts an. In den Zeilen 61 und 63 wird an beide Funktionen ein Person-Objekt übergeben, das in Zeile 59 erzeugt wird. Das ist in Ordnung, denn die Klasse Person implementiert sowohl die Schnittstelle Displayable als auch die Schnittstelle Cloneable.

Dies war ein Beispiel für das mehrfache Spezialisieren einer Schnittstelle ohne Rede­finition. Wie bei der Einfachvererbung auch gibt es drei Dinge, die man mit der oben gezeigten Syntax ausdrücken kann:

(1) Erweiterung: Die Unterklasse fügt neue Operationen hinzu.

(2) Spezialisierung ohne Redefinition: Wie (1); zusätzlich fügt die Unterklasse neue Attribute und Methoden hinzu.

(3) Spezialisierung mit Redefinition: Wie (2); zusätzlich redefiniert die Unterklas­se Methoden einer oder mehrerer Oberklassen.

Wir wollen uns im restlichen Abschnitt besonders mit den letzten beiden Punkten be­fassen, da im Rahmen der Mehrfachvererbung dort die häufigsten Fehler passieren können. Wir werden uns dabei hauptsächlich auf die Unterschiede zwischen Einfach- und Mehrfachvererbung beschränken, deshalb sollten Sie für das Verständnis dieses Abschnitts den Abschnitt über Einfachvererbung (4.6.4) gelesen haben.

4.6.5.1 Rauten & Co.Durch den Einsatz der Mehrfachvererbung ist es einer Klasse möglich, von mehr als einer Klasse zu erben. Dies haben Sie im letzten Abschnitt gesehen. Wenn diese an­deren Klassen selbst aber von derselben Klasse erben (oder dieselbe Klasse speziali­sieren), dann kann eine – für die Mehrfachvererbung durchaus typische – Situation auftreten: Eine Klasse existiert zweimal innerhalb derselben Vererbungshierarchie.

Wir wollen uns dies an einem kleinen Beispiel veranschaulichen. Wir entwerfen Schnittstellen für Kollektionen, also für Container von Objekten. Eine Kollektion – ohne dass wir etwas Näheres darüber wissen – soll nur das Hinzufügen von Elemen­ten unterstützen. Außerdem soll man über einen Iterator (6.3.2) die Elemente einer Kollektion sukzessiv ermitteln können. Diese Schnittstelle ist allgemein genug, als dass viele verschiedene, konkrete Kollektionen diese implementieren können.

Nun erweitern wir diese Schnittstelle zweimal. Zum einen wollen wir geordnete Kol­lektionen über eine geeignete Schnittstelle abbilden (die eine Operation zum Sortie­ren enthält). Zum anderen möchten wir eine Schnittstelle für Felder45 einführen, da man auf die Elemente eines Feldes bekanntlich wahlfrei zugreifen kann (d. h. ein Klient muss nicht über alle vorherigen Elemente iterieren, um an das gewünschte Element heranzukommen, sondern kann direkt sagen: Ich möchte auf das n-te Ele­ment der Kollektion zugreifen). Da es sowohl ungeordnete Felder als auch geordnete

45) Damit sind jetzt nicht C++-Felder gemeint, sondern allgemein jede Datenstruktur mit wahlfreiem Zugriff.

188

Mehrfachverer­bung als Oberbe­griff

Rauten in Klas­senhierarchien

Page 197: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

„Nicht-Felder“ gibt, sind diese beiden Schnittstellen miteinander nicht weiter ver­wandt.

Schließlich möchten wir eine konkrete Klasse für geordnete Felder implementieren. Diese Klasse ist folglich sowohl geordnet als auch ein Feld, somit muss sie beide Schnittstellen implementieren. Das Ergebnis dürfen Sie in Abbildung 43 bewundern.

Wie Sie sehen, entsteht in dieser Situation eine sogenannte Raute in der Vererbungs­hierarchie. Diese Rauten können ziemlich viele Probleme aufwerfen, sowohl für den Entwickler als auch für den Implementierer eines C++-Übersetzers. Deshalb werden sie manchmal auch mit dem Akronym DDD bezeichnet, eine Abkürzung für „Dread­ed Diamond of Death“ (übersetzt in etwa „Gefürchteter Diamant des Todes“). Wir können in diesem Skript nicht auf alle Problemstellungen eingehen, werden jedoch die wichtigsten aufzeigen.

4.6.5.2 Virtuelle BasisklassenWenn Sie das oben genannte Beispiel in C++ folgendermaßen umsetzen, werden Sie eine Überraschung erleben:

1 class Collection

189

Abbildung 43: Raute bei Mehrfachvererbung

<<interface>>OrderedCollection

Dienst „Kollektion sortieren“

<<interface>>Array

Dienst „Element zurückgeben“Dienst „Element überschreiben“

<<interface>>Collection

Dienst „Element hinzufügen“Dienst „Iterator zurückgeben“

OrderedArray

Dienst „Element hinzufügen“Dienst „Iterator zurückgeben“Dienst „Kollektion sortieren“Dienst „Element zurückgeben“Dienst „Element überschreiben“

<<realize>> <<realize>>

„Dreaded Dia­mond of Death“

Tanzt C++ aus der Reihe?

Page 198: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

2 {3 // ...4 };5 class OrderedCollection : public Collection6 {7 // ...8 };9 class Array : public Collection

10 {11 // ...12 };13 class OrderedArray : public OrderedCollection, public Array14 {15 // ...16 };

Dieser Quelltext (obwohl er korrekt aussieht) führt nicht zum gewünschten Ergebnis. Er führt vielmehr zu einer Klassenhierarchie wie in Abbildung 44.

Was ist passiert? Die Schnittstelle Collection taucht zweimal in der Klassen-Hierarchie auf! Das ist aber ein Problem, denn jeder Verweis auf Collection inner­halb der Klassenhierarchie ist mehrdeutig, z. B.

190

Abbildung 44: Rauten, die keine sind

<<interface>>OrderedCollection

Dienst „Kollektion sortieren“

<<interface>>Array

Dienst „Element zurückgeben“Dienst „Element überschreiben“

<<interface>>Collection

Dienst „Element hinzufügen“Dienst „Iterator zurückgeben“

OrderedArray

Dienst „Element hinzufügen“Dienst „Iterator zurückgeben“Dienst „Kollektion sortieren“Dienst „Element zurückgeben“Dienst „Element überschreiben“

<<realize>> <<realize>>

<<interface>>Collection

Dienst „Element hinzufügen“Dienst „Iterator zurückgeben“

Klassen tauchen mehrfach in der Hierarchie auf

Page 199: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

• wenn der Übersetzer einen OrderedArray-Verweis in einen Collection-Verweis konvertieren soll (etwa bei der Parameterübergabe an einen Algorith­mus, der mit Collection-Objekten arbeitet), so weiß der Übersetzer nicht, welche Collection-Schnittstelle gemeint ist;

• wenn eine explizite Qualifizierung einer Operation in der Collection-Schnit­stelle (beispielsweise Collection::add) durchgeführt wird.

Wünschenswert ist in fast46 allen Fällen, dass eine Basisklasse (wie Collection) nur einmal in der gesamten Klassenhierarchie vorkommt. Das ist in C++ möglich, in­dem beim Erben von dieser Klasse das Schlüsselwort virtual verwendet wird:

1 class Collection2 {3 // ...4 };5 // Achtung: Collection ist virtuelle Basisklasse!6 class OrderedCollection : virtual public Collection7 {8 // ...9 };10 // Achtung: Collection ist virtuelle Basisklasse!11 class Array : virtual public Collection12 {13 // ...14 };15 class OrderedArray : public OrderedCollection, public Array16 {17 // ...18 };

Sie sehen in Zeile 6 und 11, dass das Schlüsselwort virtual verwendet wurde. Da­durch wird erreicht, dass die Klasse Collection nun nur einmal in der Klassen­hierarchie vorkommt. Allerdings gibt es einiges zu dieser Konstruktion zu sagen:

• Das Schlüsselwort virtual an dieser Stelle hat nichts, aber auch gar nichts mit der Verwendung zur Kennzeichnung polymorpher Operationen zu tun. Wie schon bereits in Abschnitt 2.3 erwähnt wurde, vermeidet C++ das Einführen neu­er Schlüsselwörter, wo es nur geht, leider auch zum Teil auf Kosten der Ver­ständlichkeit. Hier wird virtual lediglich verwendet, um dem Übersetzer mit­zuteilen, dass die Basisklasse nur dann in die Klassenhierarchie eingefügt werden soll, wenn sie dort nicht bereits existiert.

• Es ist unwesentlich, ob das Schlüsselwort virtual vor oder nach dem Schlüs­selwort public steht.

• Eine Klasse, von der mit Hilfe des Schlüsselworts virtual abgeleitet wird, nennt man in C++ virtuelle Basisklasse.

46) Ja, es gibt wirklich Situationen, in denen das doppelte Auftauchen derselben Klasse in der Hierar­chie Sinn machen kann, allerdings ist der Entwurf eine solchen Klassenhierarchie nicht unbedingt gut und es gibt immer Mittel und Wege, dasselbe Ziel ohne solche Konstruktionen zu erreichen, was zu übersichtlicheren und verständlicheren Entwürfen führt.

191

mehrfache Exis­tenz einer Klasse fast immer uner­wünscht

Regeln in Bezug auf den virtu­al-Modifizierer bei Vererbung

Page 200: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

• Das Schlüsselwort muss bei jeder Spezialisierung der Klasse Collection ver­wendet werden! Für jede Ableitung, bei der Sie das Schlüsselwort virtual nicht verwenden, wird eine zusätzliche Instanz der Klasse in die Vererbungshier­archie einfügt. Sie sollten sich also merken: Einmal virtuelle Basisklasse, immer virtuelle Basisklasse. Diese Erkenntnis ist in einem Merksatz am Ende des Ab­schnitts zusammengefasst.

• Virtuelle Basisklassen haben gewissermaßen einen Sonderstatus in C++, weil man mit ihnen nicht alles machen kann, was mit „normalen“ Basisklassen mög­lich ist. Es ist beispielsweise nicht möglich, einen Zeiger auf ein Objekt einer virtuellen Basisklasse in einen Zeiger auf ein Objekt einer abgeleiteten Klasse zu konvertieren.47 Außerdem haben virtuelle Basisklassen einen erhöhten Laufzeit-Aufwand zur Folge, so dass das intensive Benutzen virtueller Basisklassen sich unter Umständen negativ auf die Performanz des Programms auswirken kann. Deshalb sollten Sie virtuelle Basisklassen wirklich nur dann nutzen, wenn es sein muss (etwa im obigen Fall).

Merksatz 27: Vermeide den Einsatz virtueller Basisklassen!

Merksatz 28: Einmal virtuelle Basisklasse, immer virtuelle Basisklasse!

4.6.5.3 Redefinition einer Methode und DominanzDie Redefinition einer Methode funktioniert bei der Mehrfachvererbung im Prinzip genauso wie bei Einfachvererbung. Allerdings muss jetzt nicht nur gesichert sein, dass es für jede Operation mindestens eine, sondern auch höchstens eine Methode gibt. Abbildung 45 veranschaulicht dies:

47) Es ist nicht schlimm, wenn Sie dies nicht nachvollziehen können – virtuelle Basisklassen sind nun mal ein etwas obskures Gebiet in C++, in das man sich nur hineinwagen sollte, wenn man unbe­dingt muss.

192

die Frage nach der Dominanz

Abbildung 45: Beispiel für Dominanz von Methoden

List

void add (Element e)Element getFirst ()Element getNext (Element e)

Array

void add (Element e)Element get (int index)void set (int index, Element e)

<<interface>>Collection

void add (Element e)

ArrayList

<<realize>> <<realize>>

Page 201: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

Wir haben eine Schnittstelle Collection und zwei Klassen List und Array, die jeweils eine verkettete Liste und ein Feld implementieren. Nun dachte sich ein Ent­wickler, Mehrfachvererbung wäre doch eine tolle Sache, um eine Klasse zu bekom­men, deren Objekte sich wie Felder und wie Listen verhalten, so dass man über beide Schnittstellen darauf zugreifen kann. Dieser Klasse, ArrayList genannt, hat der Entwickler keine weiteren Methoden spendiert, weil diese bereits alle schon „da“ sind, und zwar über die Basisklassen Array und List.

Nun entsteht aber offensichtlich ein Problem. Gehen wir davon aus, wir haben ein Objekt der Klasse ArrayList und schicken ihm die Nachricht add (geeignet para­metrisiert mit einem Element-Objekt):

1 ArrayList arrayList;2 Element e;3 arrayList.add (e); // <-- Problem!

Die Preisfrage ist: Welche Methode wird in Zeile 3 ausgeführt? Es kann die Methode der Klasse Array sein, oder die der Klasse List. Keine davon ist besser oder schlechter geeignet als die jeweils andere. Keine der Methoden ist dominant, es liegt also eine Mehrdeutigkeit vor, die der Übersetzer dann auch mit einer entsprechenden Fehlermeldung quittiert.

In einem solchen Fall muss der Entwickler dem Übersetzer „das Denken abnehmen“ und in der Klasse ArrayList die Methode add explizit redefinieren. In deren Im­plementierung wird der Entwickler sich dann für den Aufruf der einen oder anderen geerbten Methode (oder vielleicht für den Aufruf beider Methoden?) entscheiden. Diese Entscheidung kann ihm der Übersetzer aber nicht abnehmen, da es keine ein­deutige Lösung gibt.

4.6.5.4 Konstruktoren und Destruktoren bei MehrfachvererbungFür Konstruktoren und Destruktoren gilt das bereits in 4.6.4.4 und 4.6.4.5 Gesagte, allerdings gibt es im Falle von Mehrfachvererbung mehr als eine Basisklasse, so dass unter Umständen mehr als ein Konstruktor explizit aufgerufen werden muss. Bei­spiel:

1 // expliziter Default-Konstruktor der Klasse Array-List2 ArrayList::ArrayList ()3 :4 List (), // ruft explizit den Default-Konstruktor der Basisklasse List auf5 Array () // ruft explizit den Default-Konstruktor der Basisklasse Array auf6 {7 }

Das Beispiel verdeutlicht, dass die Syntax für den Aufruf von Konstruktoren dieselbe ist wie bei der Einfachvererbung. Es ist – ebenfalls wie bei der Einfachvererbung – zu beachten, dass nur Konstruktoren direkter Basisklassen aufgerufen werden dürfen. Allerdings gibt es bei virtuellen Basisklassen eine Ausnahme, die weiter unten erör­tert wird.

193

Welche Methode soll benutzt wer­den?

explizite Redefini­tion manchmal notwendig

Aufruf mehrerer Konstruktoren im Konstruktor einer Unterklasse

Page 202: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

Bei der Mehrfachvererbung gibt es eine ähnliche Gefahr bei der Initialisierung von Ba­sisklassen wie im Kontext der Einfachvererbung bei der Initialisierung von Attributen: Die Konstruktoren werden nicht in der Reihenfolge der Aufrufe im Konstruktor der Ba­sisklasse aufgerufen, sondern in der Reihenfolge der public-Konstrukte im Klassen­kopf. Beispiel:

1 // Achtung: erst Array, dann List im Klassenkopf2 class ArrayList : public Array, public List3 {4 public :5 ArrayList ();6 virtual void add (Element e); // nötig wegen Dominanz-Regel7 };89 ArrayList::ArrayList ()

10 :11 List (), // Achtung: erst Aufruf des List-Konstruktors,12 Array () // dann Aufruf des Array-Konstruktors!13 {14 }

In Zeile 11 und 12 werden die Konstruktoren der Basisklassen aufgerufen, allerdings in einer anderen Reihenfolge als die Klassen in Zeile 2 im Klassenkopf vermerkt sind. Die Reihenfolge in Zeile 2 ist jedoch die wesentliche, somit wird im ArrayList-Kon­struktor in Wirklichkeit erst der Aufruf in Zeile 12 und dann der Aufruf in Zeile 11 durchgeführt.

Bei virtuellen Basisklassen ist äußerste Vorsicht geboten: In C++ werden virtuelle Ba­sisklassen immer vor allen nicht-virtuellen Basisklassen initialisiert und nur von der speziellsten Klasse aufgerufen. Beispiel:

1 // Container-Abstraktion2 // Operationen aus Gründen der Übersichtlichkeit weggelassen3 class Collection4 {5 public :6 Collection (int size);7 private :8 int m_size;9 };

10 Collection::Collection (int size)11 :12 m_size (size)13 {14 }1516 // Feld-Abstraktion17 // Achtung: Collection ist virtuelle Basisklasse!18 class Array : virtual public Collection19 {20 public :21 Array (int size);22 };23 Array::Array (int size)24 :25 Collection (size)26 {27 }2829 // Listen-Abstraktion30 // Achtung: Collection ist virtuelle Basisklasse!31 class List : virtual public Collection

194

Reihenfolge der Initialisierung von Basisklassen

virtuelle Basis­klassen und Kon­struktoren

Page 203: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

32 {33 public :34 List (int size);35 };36 List::List (int size)37 :38 Collection (size)39 {40 }4142 // kapselt Objekte, die sowohl Felder als auch Listen sind43 // Achtung: Collection ist (indirekt) virtuelle Basisklasse!44 class ArrayList 45 : virtual public Array, virtual public List46 {47 public :48 ArrayList (int size);49 };50 ArrayList::ArrayList (int size)51 :52 // Achtung: drei verschiedene Größen (siehe Text)!!!53 // Achtung: unpassende Reihenfolge (siehe Text)54 Array (size + 2),55 List (size + 3),56 Collection (size + 1)57 {58 }

Dieses etwas konstruierte Beispiel soll das Besondere bei der Initialisierung virtueller Basisklassen demonstrieren. In diesem Beispiel wird der Konstruktor der Klasse Col­lection an drei verschiedenen Stellen, nämlich in den Zeilen 25, 38 und 56, aufgeru­fen. Wenn ein Objekt der Klasse Array erzeugt wird, wird der Aufruf in Zeile 25 be­nutzt, bei List-Objekten derjenige in Zeile 38. Dies bereitet keine Probleme. Bei ArrayList-Objekten ist die ganze Geschichte jedoch nicht mehr so einfach. Folgende Beobachtungen drängt der obige Quelltext auf:

• Fehlte der Aufruf in Zeile 56, wüsste der Übersetzer nicht, wie er die Collecti­on-Basisklasse für ein ArrayList-Objekt initialisieren sollte. Ganz ähnlich wie in Abschnitt 4.6.5.3 fehlt hier ein dominanter Konstruktor-Aufruf48. Deshalb ist die Initialisierung in Zeile 56 notwendig.

• Jetzt wird der Übersetzer mit drei Initialisierungen konfrontiert, von denen die in Zeile 56 dominant ist. Deshalb werden alle anderen Initialisierungen derselben vir­tuellen Basisklasse (also jene in den Zeilen 25 und 38) ignoriert. Insbesondere werden die Argumente, die in den Zeilen 54 und 55 an den Collection-Kon­struktor durchgeschleift werden sollen, überhaupt nicht benutzt!

• Virtuelle Basisklassen werden immer zuerst initialisiert. Deshalb findet der Kon­struktor-Aufruf in Zeile 56 vor denen in den Zeilen 54 und 55 statt. Man sollte folglich Aufrufe von Konstruktoren virtueller Basisklassen immer an den Anfang der Initialisierungsliste stellen.

Alles in allem ist die Benutzung konkreter (d. h. nicht abstrakter) virtueller Basisklassen (insbesondere jener mit Konstruktoren und Zuweisungsoperatoren) derart problema­tisch, dass generell davon abgeraten wird.49 Die vorherrschende Meinung unter C++-Entwicklern ist, dass virtuelle Basisklassen reine Schnittstellen-Klassen sein sollten

48) Bei virtuellen Basisklassen von Dominanz zu sprechen ist im C++-Jargon nicht unbedingt üblich, der Begriff drückt jedoch gut die Problematik bei virtuellen Basisklassen aus.

49) Es kann passieren, dass Zuweisungsoperatoren für virtuelle Basisklassen mehrfach ausgeführt wer­den!

195

konkrete virtuelle Basisklassen sind hochgradig pro­blematisch

Page 204: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

(also ohne Methoden und Konstruktoren). (Virtuelle) Destruktoren stellen jedoch kein Problem dar, da diese nicht zu Mehrdeutigkeiten führen und auch (in der Regel) vom Übersetzer und nicht vom Benutzer aufgerufen werden.

Merksatz 29: Vermeide konkrete virtuelle Basisklassen!

Die Regeln zu Destruktoren unterscheiden sich nicht von denen bei der Einfachverer­bung. Insbesondere werden diese in umgekehrter Reihenfolge der Konstruktor-Auf­rufe bei der Initialisierung ausgeführt. Im Falle von Mehrfachvererbung und virtuel­len Basisklassen (4.6.5.2) bedeutet dies, dass Destruktoren virtueller Basisklassen immer zuletzt aufgerufen werden

4.6.6 Korrekte Anwendung von VererbungIn diesem Abschnitt befassen wir uns näher mit dem Vererbungs-Mechanismus, den wir in den letzten Abschnitten kennen gelernt haben. Wir werden sehen, dass die Be­griffe Spezialisierung und Vererbung in der Informatik nicht immer mit den „natürli­chen“ übereinstimmen.

4.6.6.1 Beispiel 1: Rechteck und QuadratBetrachten wir einmal folgendes Beispiel aus der Mathematik. Wir wissen alle, was ein Quadrat und was ein Rechteck ist. Betrachten wir einmal die Beziehung zwischen Quadrat und Rechteck. Wir können sagen, dass ein Quadrat ein spezielles Rechteck ist, nämlich eines, bei dem alle Seiten gleich lang sind. Die Klasse aller Quadrate ist also eine Teilmenge der Klasse aller Rechtecke, charakterisiert über diese spezielle Eigenschaft der Gleichheit aller Seiten. Diese Spezialisierung können wir – wie ge­wohnt – in UML ausdrücken (Abbildung 46, links).

Bis jetzt ist „die Welt in Ordnung“. Nun machen wir uns Gedanken über sinnvolle Operationen auf Objekten dieser beiden Klassen. Gewiss machen bei Rechtecken Operationen zur Abfrage der beiden Seitenlängen Sinn, ebenso die Abfrage der (ei­nen) Seitenlänge bei einem Quadrat (Abbildung 46, Mitte).

Wir wollen jedoch unsere Objekte auch verändern können. Denken Sie zurück an un­seren graphischen Editor – bei Rechtecken wollen wir auch in der Lage sein, die Sei­tenlängen zu verändern, um z. B. dem Befehl des Anwenders, ein Rechteck zu stre­cken, nachzukommen. Also fügen wir die entsprechenden Operationen zum Setzen der Höhe und Breite eines Rechtecks hinzu (Abbildung 46, rechts).

Und jetzt fangen unsere Probleme an! Gehen wir davon aus, dass wir zum Strecken eines Rechtecks die folgende Funktion definiert haben:

1 /*2 * Streckt das übergebene Rechteck in seiner Breite um den3 * angegebenen Faktor.4 */5 void stretchHorizontally (Rectangle &rect, double factor)6 {7 rect.setWidth (rect.getWidth () * factor);8 }

196

Wann ist der Ein­satz von Verer­bung angebracht?

Wann sind Qua­drate wirklich Rechtecke?

Strecken von Rechtecken ist tödlich für die Spezialisierung!

Page 205: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

Diese Funktion kann beispielsweise folgendermaßen definiert werden (die Existenz eines geeigneten Rectangle-Konstruktors natürlich vorausgesetzt):

9 int main ()10 {11 // Rechteck erzeugen12 Rectangle rect (/*Breite*/ 10,/*Höhe*/ 5);13 // Rechteck strecken14 stretchHorizontally (rect, 2);15 // rect.getWidth() == 20, rect.getHeight() == 5

So weit, so gut. Was passiert aber im folgenden Code-Fragment?16 // Quadrat erzeugen17 Square square (/*Breite und Höhe*/ 10);18 // Quadrat strecken?!19 stretchHorizontally (square, 2);20 // square.getWidth() == 20, square.getHeight() == 10!21 return 0;22 }

Nach dem Strecken ist unser Quadrat gar kein Quadrat mehr! Oder vielleicht doch? Zuerst müssen wir klären, ob das obige Programm überhaupt korrekt ist. Nach unse­rem Verständnis von Spezialisierung können wir etwas konkreteres oder spezielleres immer dann benutzen, wenn etwas abstrakteres oder allgemeineres erforderlich ist. Ein Quadrat ist ein spezielles Rechteck (oder: ein Quadrat IST EIN Rechteck, um auf die IST-EIN-Beziehung zurückzukommen), also ist das Übergeben eines Quadrats an die Funktion stretchHorizontally erst einmal korrekt.

Was aber seltsam anmutet ist die Verwendung der Operation setWidth auf Qua­draten. Bei Rechtecken ist es verständlich, dass ein Klient die Breite eines Rechtecks verändern kann. Bei einem Quadrat hingegen ist das problematisch: Wenn jemand die Breite eines Quadrats verändert, muss die Höhe ebenfalls mitgeändert werden, denn ansonsten erhalten wir ein Quadrat, das keines ist!

Im obigen Fall wurde die Breite unabhängig von der Höhe verändert, was zu dem un­erwünschten Ergebnis square.getWidth() != square.getHeight()

197

Abbildung 46: Spezialisierung zwischen Quadrat und Rechteck

Square

Square

Square

Rectangle

int widthint height

Rectangle

getWidth(): intgetHeight(): int

int widthint height

Rectangle

getWidth(): intgetHeight(): intsetWidth(int width)setHeight(int height)

int widthint height

Page 206: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

führte. Ändern wir die Semantik von setWidth auf Square-Objekten dementspre­chend ab, dass eine setWidth-Nachricht zu einer äquivalenten setHeight-Nachricht auf demselben Objekt führt (und umgekehrt), sind wir aber kein Stück bes­ser dran! Warum, das zeigt der nächste Code-Abschnitt:

1 void checkedSetWidth (Rectangle &rect, int newWidth)2 {3 int oldHeight = rect.getHeight ();4 rect.setWidth (newWidth);5 if (oldHeight != rect.getHeight ())6 error ();7 }

Hier ist eine Funktion checkedSetWidth definiert, die auf der Tatsache aufbaut, dass die Änderung der Breite eines Rechtecks keine Auswirkungen auf die Höhe ha­ben darf – wenn doch, wird eine Fehlerbehandlungs-Funktion (error) aufgerufen. Nun ist diese Funktion vielleicht nicht die scharfsinnigste oder hilfreichste, aber sie ist völlig korrekt – sie darf diese Annahme machen, solange der Kommentar der setWidth-Operation nichts anderes sagt! Und das sollte auch nicht der Fall sein; schließlich würde es den armen Programmierer (und das sind Sie!) nur verwirren, wenn er wüsste, dass setWidth manchmal zu setHeight führt und umgekehrt (bei Quadraten) und manchmal nicht (bei Rechtecken). Wie die Namen der Operatio­nen schon ausdrücken, sollte setWidth nur die Breite verändern und setHeight nur die Höhe. Punkt.

Die obige checkedSetWidth-Funktion wird also bei Quadraten nicht korrekt funktionieren (bzw. die error-Funktion aufrufen), wenn eine Änderung der Breite zu einer Änderung der Höhe führt; und ein Aufruf der setWidth-Operation wird bei Quadraten zu seltsamen Zuständen führen, wenn er nicht von einem entsprechen­den setHeight-Aufruf gefolgt wird. Egal wie wir es drehen und wenden, es klappt einfach nicht: Irgend etwas wird nicht korrekt funktionieren.

Was schließen wir daraus? Ein Quadrat mag in der Mathematik durchaus ein speziel­les Rechteck sein, in der Informatik hapert diese Klassifizierung jedoch, sobald Ver­halten zu den Klassen hinzukommt (in unserem Beispiel setWidth und setHeight). Wir lernen: Es kommt darauf an, die richtige Frage zu stellen. Nicht: IST ein Quadrat EIN Rechteck? Sondern: VERHÄLT sich ein Quadrat WIE ein Rechteck? Die letzte Frage müssen wir mit einem klaren „Nein!“ beantworten, denn mit Rechtecken können wir wesentlich mehr nützliche Dinge tun, die es als Rechteck belassen, während ein Quadrat dabei nicht immer ein Quadrat bleibt (etwa Strecken und Stauchen). Es ist also Verhalten, das entscheidet, ob eine Klasse spezieller ist als eine andere und ob der Einsatz von Spezialisierung (oder Vererbung) in einem kon­kreten Fall Sinn macht.

4.6.6.2 Beispiel 2: Elemente und Listen derselbenEin weiteres Beispiel können wir unseren graphischen Objekten aus diesem Kapitel entnehmen. Wir haben die Objekte so modelliert, so dass gilt: ein Punkt IST EIN graphisches Objekt (ebenso bei Linien, Kreisen etc.) Wir haben die Spezialisierung zwischen Point und GraphicalObject dadurch begründet, dass sich jeder

198

Verhalten ist das Wesentliche

Listen und spezi­elle Listen

Page 207: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

Punkt wie ein graphisches Objekt verhält (wenn man die Schnittstelle Graphical­Object betrachtet). Die Spezialisierung ist korrekt und funktioniert fabelhaft.

Jetzt stellen wir uns aber vor, dass wir graphische Objekte in einer Liste aufbewahren wollen. Nennen wir diese Klasse GraphicalObjectList; Objekte zu dieser Klasse repräsentieren Ansammlungen von GraphicalObject-Objekten. Solch eine Liste kann also beliebige graphische Objekte enthalten, z. B. einen Kreis, zwei Punkte und vier Linien. Oft ist es wünschenswert, dass man aber festlegen kann, dass sich in einer Liste z. B. nur Punkte oder nur Linien befinden. Zu diesem Zweck ent­werfen wir entsprechende Klassen, die wir PointList (Liste aus Punkten), Line­List (Liste aus Linien) u. s. w. nennen.

So weit, so gut. Nun kommt der entscheidende Schritt. Wir sagen: „Ein Point-Ob­jekt IST EIN GraphicalObject-Objekt“. Gilt denn nun auch: „Ein Point­List-Objekt IST EIN GraphicalObjectList-Objekt“ (Abbildung 47)?

Einige Anmerkungen zum Diagramm: Zwischen PointList und GraphicalOb­jectList herrscht eine Vererbungsbeziehung, da die Klasse GraphicalObject­List (im Gegensatz zu GraphicalObject) eine konkrete Klasse ist, zu der auch Objekte existieren können – nämlich jene Listen, die irgendwelche GraphicalOb­ject-Objekte enthalten. Der Name der Beziehung zwischen PointList und Point wird mit einem Schrägstrich eingeleitet, was darauf hinweist, dass diese Beziehung ab­geleitet ist: Es ist nämlich dieselbe Beziehung wie die zwischen GraphicalObject­List und GraphicalObject, bloß aus der Sicht von PointList. Das ist auch lo­gisch: Wenn meine Oberklasse bereits angibt, dass eine Verbindung zu anderen Objekten existiert, dann existiert diese Verbindung bei Objekten zu einer Unterklasse erst recht. Schließlich werden Assoziationen genauso mitvererbt wie Attribute.

Zurück zum eigentlichen Problem: Ist die angegebene Vererbung korrekt in dem Sin­ne, dass jedem Klienten, der ein Objekt der Klasse GraphicalObjectList er­wartet, ein Objekt der Klasse PointList „untergeschoben“ werden kann und das

199

Abbildung 47: Spezialisierung zwischen PointList und GraphicalObjectList

<<interface>>GraphicalObject

show ()hide ()move ()

Point

show ()hide ()move ()

<<re

aliz

e>>

GraphicalObjectList

void add (GraphicalObject object)GraphicalObject get (index)void remove (index)bool isEmpty ()

PointList

void add (Point object)Point get (index)

element

* ◄ contains

/ element

* ◄ contains

abgeleitete Bezie­hungen in der UML

Page 208: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

Programm weiterhin funktioniert? Denken Sie ruhig ein paar Minuten nach, bevor Sie weiterlesen...

Die Antwort ist: Nein, Sie bekommen mit dem obigen Modell Probleme beim Hinzu­fügen von Elementen. Nehmen wir einmal an, wir haben folgende Funktion:

1 void addToList (GraphicalObjectList &list, GraphicalObject &elem)2 {3 list.add (elem);4 }

Eine kleine und harmlose (?) Funktion. Jetzt können Sie, konform zu Ihrem Modell, Folgendes tun: Sie können dieser Funktion ein PointList-Objekt und ein Line-Objekt übergeben:

5 int main ()6 {7 PointList pointList;8 Line line (Point (1, 2), Point (3, 4));9 addToList (pointList, line); // !!!!!!!!!!

10 return 0;11 }

Überlegen Sie einmal genau, was in Zeile 9 passiert! Sie fügen eine Linie in eine Lis­te mit Punkten ein. Das ist aber etwas, was Sie eigentlich verbieten wollten. Nur eine GraphicalObjectList-Liste kann beliebige graphische Objekte verwalten; eine PointList-Liste soll (natürlich) nur Punkte aufnehmen können.

Die Moral von der Geschichte: Diese Vererbungsbeziehung ist inkorrekt. Ein PointList-Objekt verhält sich nicht wie ein GraphicalObjectList-Objekt, weil es strengere Vorbedingungen hat: Während die Argumente von Graphical­ObjectList::add alle möglichen GraphicalObject-Objekte sein können, dürfen es bei PointList::add nur Point-Objekte sein. Das aber bedeutet, dass ich nicht jedem Klienten, der ein GraphicalObjectList-Objekt erwartet, ein PointList-Objekt übergeben kann, weil dann diese strengere Bedingung eventuell nicht eingehalten wird. Die Stärken der Polymorphie sind ausgehebelt, und Chaos kann sich ausbreiten (in unserem Beispiel eine PointList-Liste mit Linien darin).

Dass die Spezialisierung zwischen PointList und GraphicalObjectList nicht korrekt ist, können Sie noch an zwei weiteren Dingen erkennen. Zum einen ist die Methode add der Klasse PointList keine Redefinition der Methode add in der Klasse GraphicalObjectList, weil der Parametertyp ein anderer ist (siehe hierzu Abschnitt 4.6.4.1). Zum anderen hat die Implementierung der Methode PointList::get ein Problem: Sie wird – um das Objekt aus der Liste zu holen – vermutlich GraphicalObjectList::get in Anspruch nehmen. Diese Opera­tion liefert jedoch einen Zeiger auf ein GraphicalObject-Objekt zurück. Da die Methode PointList::get jedoch einen Zeiger auf ein Point-Objekt zurücklie­fern muss, wird ihr nichts anderes übrig bleiben, als den Zeiger in den erwarteten Typ explizit umzuwandeln (3.4.5.2). Explizite Typ-Umwandlungen sind jedoch zu vermeiden; hier sind sie Ausdruck eines schlechten Entwurfs.

200

spezielle Listen sind problema­tisch

weitere Anzei­chen inkorrekter Spezialisierung

Page 209: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Welt der Objekte

4.6.6.3 ZusammenfassungDie Quintessenz der beiden Beispiele sollten Sie unbedingt verinnerlichen. Sie be­sagt nämlich, dass die Strukturierung von Systemen über das Verhalten geregelt wer­den sollte und nicht über gemeinsame Daten (Attribute). Sie besagt auch, dass in den meisten Fällen Spezialisierung (und noch schlimmer Vererbung) fehlerhaft angewen­det wird, um z. B.

• (Spezialisierung) IST-EIN-Beziehungen in der realen Welt abzubilden,

• (Vererbung) gemeinsame Attribute in einer Oberklasse zusammenzufassen,

• (Vererbung) gemeinsame Methoden in einer Oberklasse zusammenzufassen,

ohne das Verhalten der Objekte zu berücksichtigen. Sie besagt schließlich, dass Sinn oder Unsinn von Spezialisierung und Vererbung kontextabhängig ist. Die obigen Beispiele sind korrekt, solange Sie keine setWidth-/setHeight- oder Strecken-/Stauchen-Operationen in Rectangle bzw. keine add-Operation in Graphical­ObjectList einbauen.

Alles in allem: Halten Sie die Augen offen, wenn Sie Spezialisierung und Vererbung einsetzen, und machen Sie sich stets klar, dass die Objekte nicht nur existieren, son­dern auch leben, und als solche „Lebewesen“ auch ein Verhalten an den Tag legen. Und dieses Verhalten muss zu jedem Typ, jeder Schnittstelle passen, durch diese sie ein solches Objekt gerade betrachten. Um zum Schluss auf das erste Beispiel zurück­zukommen: Ein Quadrat durch die Rechteck-„Brille“ zu betrachten kann manchmal ganz schön problematisch sein...

Merksatz 30: Korrektheit einer Spezialisierung ist vom Verhalten abhängig!

Merksatz 31: Bedenke den Einsatz von Vererbung!

4.7 Literaturempfehlungen[Oest01] legt den Schwerpunkt auf die Konstrukte der Beschreibungssprache UML. Das Buch enthält aber ebenso viele Informationen zum objektorientierten Paradigma und zur objektorientierten Software-Entwicklung. Insbesondere handelt es sich nicht um ein C++-Buch, da die behandelten objektorientierten Konzepte sich in vielen Pro­grammiersprachen wiederfinden.

[GHJV95] ist das Standard-Werk zum objektorientierten Entwurf. Das Buch ist die erste Veröffentlichung, die sich ausschließlich mit der Klassifikation und ausführli­chen Beschreibung von Entwurfsmustern (6) beschäftigt, enthält aber auch viele all­gemeine Informationen über gute objektorientierte Entwürfe. Die Code-Beispiele in dem Buch sind zu großen Teilen in C++ verfasst.

4.8 ÜbungenÜ19 (*2) Schreiben Sie das Beispiel queue1 aus Abschnitt 4.4.5.2 so um, dass es

dem Entwurf in Abbildung 29 entspricht!

201

Einsatz von Spe­zialisierung und Vererbung will gut bedacht sein

Page 210: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Die Welt der Objekte Objektorientiertes C++ für Einsteiger

Ü20 (*2) Vervollständigen Sie das Beispiel graphobj aus Abschnitt 4.6.1 um die Definition und Implementierung der Klassen Circle und Rectangle!

Ü21 (*5) Bauen Sie das Beispiel aus der vorigen Übung zu einem einfachen graphi­schen Editor von geometrischen Objekten aus! Erweitern Sie die Klassen ggfs. um weitere Operationen und Algorithmen!

Ü22 (*2,5) Erweitern Sie das Beispiel oohello aus Abschnitt 4.2 derart, dass Sie zwei weitere Begrüßungs-Klassen FoermlicheBegruessung und Engli­scheBegruessung definieren, die auf die Nachricht begruesse mit den Meldungen „Sehr geehrte(r) person“ bzw. „Hello person“ reagieren (wobei person ein Platzhalter für die übergebene Person darstellt)! Erzeugen Sie eine geeignete Schnittstellen-Klasse als Abstraktion der drei Begrüßungs-Klassen! Erlauben Sie dem Benutzer, über eine Funktion die gewünschte Begrüßung aus­zuwählen! Verwenden Sie dabei die Vorteile von Polymorphie, um die Abhän­gigkeit zur gewählten Begrüßung möglichst gering zu halten!

Ü23 (*2,5) Implementieren Sie das Video-Beispiel aus Abschnitt 4.1 so, wie es im Entwurf in Abbildung 27 zu sehen ist! Implementieren Sie dabei die entspre­chenden Methoden, indem Sie geeignete Ausgaben über die durchgeführten Aktionen ausgeben (etwa „Ziehe Medium ein“)! Erweitern Sie die Klassen ggfs. um weitere Operationen und Methoden!

Ü24 (*2,5) Stellen Sie einstellige mathematische Funktionen (also Funktionen mit genau einem Parameter), etwa die Sinus-Funktion, als Klassen dar! Jede dieser Klassen soll eine Operation zum Berechnen der jeweiligen Funktion enthalten! Überlegen Sie, wie Sie mit Hilfe von zwei zusätzlichen Klassen die Funktionen beliebig komponieren können! (Die Komposition zweier Funktionen ist die ma­thematische Verkettung: Der mathematische Ausdruck sin(cos(tan(x))) ist die Anwendung der Verkettung der drei Funktionen sin, cos und tan auf das Argu­ment x. Es wird also zuerst tan(x) berechnet, dann auf das Ergebnis die cos-Funktion angewandt und schließlich dieses Resultat als Argument der sin-Funk­tion verwendet.) Hinweis: Sie werden eine geeignete Abstraktion benötigen!

Ü25 (*3) Modellieren und implementieren Sie ein Programm zur Verwaltung von Stücklisten! Die Produkte bestehen dabei aus Produktteilen, die ihrerseits aus weiteren Teilen bestehen, bis irgendwann elementare Einzelteile vorliegen. Der Preis eines Produkts bzw. eines Produktteils berechne sich aus der Summe der Preise aller Teile, zuzüglich eines frei zu definierenden „Mehrwerts“. Imple­mentieren Sie in Ihren Klassen eine Operation berechnePreis, die für alle Produkte den Preis berechnet! Hinweis: Sie werden für elementare Einzelteile und zusammengesetzte Produkte eine geeignete Abstraktion benötigen!

Ü26 (*3) Entwerfen Sie eine Klassenhierarchie, die arithmetische Ausdrücke abbil­det! Es soll also Klassen wie Konstante, Addition oder Division ge­ben, wobei die Klasse Addition auf die Unterausdrücke verweist, die sie ad­diert. Implementieren Sie in jeder Klasse eine Operation, die den Ausdruck ausrechnet! Hinweis: Sie werden eine geeignete Abstraktion Ausdruck benö­tigen!

202

Page 211: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Fehler erkennen und behandeln

5 Fehler erkennen und behandelnDieses Kapitel geht näher auf die Sprachelemente von C++ ein, die für die Behand­lung von Fehlern existieren. Es wird geklärt, was Ausnahmen sind und wie sie sinn­voll verwendet werden können. Das Kapitel geht auch auf die Rolle der Ausnahme-Sicherheit ein und wie sie erreicht werden kann. Schließlich wird die Interaktion von Ausnahmen mit dem RAII-Idiom (4.5.2) betrachtet.

5.1 GrundlegendesFehlerbehandlung ist ein wichtiges und dennoch häufig vernachlässigtes Element je­der Software-Entwicklung. Bevor wir jedoch tiefer in das Thema einsteigen, müssen wir zuerst einige zentrale Begriffe klären. Dies ist wichtig, weil diese Begriffe häufig ziemlich lax verwendet werden, aber für die folgenden Abschnitte von zentraler Be­deutung sind. Die Definitionen sind [Zell03] entnommen.

• Fehler: Ein Fehler ist eine Abweichung vom Korrekten, Richtigen, Wahren.

• Defekt: Ein Defekt ist ein Fehler innerhalb eines Programms. Defekte werden gemeinhin als Bugs bezeichnet. Gelegentlich wird ein Defekt ein innerer Fehler genannt.

• Störung / Versagen: Eine Störung liegt vor, wenn ein Defekt zur Ausführung gelangt und damit den ordnungsgemäßen Ablauf des Programms verhindert. Stö­rungen sind somit „von außen“ sichtbare Fehler, deshalb werden sie manchmal auch als äußere Fehler bezeichnet. Wichtig ist festzuhalten, dass nicht unbedingt jeder Defekt zu einer Störung führen muss – etwa wenn er sich nur bei bestimm­ten Daten manifestiert, die aber während eines Programmlaufs nicht auftreten.

• Irrtum: Ein Irrtum ist eine menschliche Handlung oder Entscheidung, die zu ei­nem Fehler führt.

• Ausnahme: Eine Ausnahme stellt ein Ereignis dar, das zum Unterbrechung des normalen Programmablaufs führt.

Zusammengefasst führen also Irrtümer zu Fehlern, die sich in Programmen als De­fekte manifestieren und bei ihrer Ausführung Störungen zur Folge haben.

Im Rahmen dieses Kapitels geht es uns insbesondere um Fehlersituationen, die durch die Interaktion des Programms mit seiner Außenwelt – seien es der Benutzer, die Laufzeit-Umgebung, das Betriebssystem – entstehen. Diese Situationen müssen vom Programm erkannt und geeignet behandelt werden. Ansonsten kann sich ein Defekt einschleichen, der zur Laufzeit zu einer Störung führt.

Ein Beispiel ist ein Programm, das zwei vom Benutzer eingegebene Zahlen durch­einander dividiert und das Ergebnis ausgibt. Vergisst der Programmierer den Divisor daraufhin zu prüfen, dass er ungleich Null ist, ist dies ein Irrtum, der zu einem De­fekt führt (der Abwesenheit einer notwendigen Prüfung). Dieser Defekt äußert sich zur Laufzeit in einer Störung, sobald der Benutzer als Divisor Null wählt. Die Stö­rung kann sich z. B. in einem geregelten Programmabbruch mit entsprechender Mel­dung oder einem unkontrollierten „Absturz“ äußern.

203

grundlegende Be­griffe

Fokus liegt auf Fehlern, die „von außen“ kommen

Page 212: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Fehler erkennen und behandeln Objektorientiertes C++ für Einsteiger

In C++ gibt es ein einfaches Modell, um mit solchen Situationen im Programm um­zugehen: Immer wenn ein Fehler festgestellt wird, erzeugt das Programm eine Aus­nahme. Diese Ausnahme ist in der Regel ein Objekt, das Informationen zum Auftre­ten des Fehlers und zur Fehlerursache (sofern bekannt) beinhaltet. Ist ein solches Ausnahme-Objekt erzeugt, verlässt das Programm den normalen Programmablauf und reicht die Ausnahme an einen sogenannten Ausnahme-Behandler weiter. Dieser hat dann die Möglichkeit, das Ausnahme-Objekt auszuwerten und geeignete Maß­nahmen durchzuführen (etwa die Ausgabe einer Meldung, dass ein Divisor nicht Null sein kann). Ist der Ausnahme-Behandler fertig, kann das Programm ab diesem Punkt normal ausgeführt werden. Das Weiterreichen einer Ausnahme wird im C++-Kontext (von den Bedeutungen der entsprechenden C++-Schlüsselwörter abgeleitet) häufig Auswerfen, das Behandeln Fangen einer Ausnahme genannt.

Ausnahme-Behandler haben immer einen bestimmten „Arbeitsbereich“, d. h. sie sind nur für bestimmte Ausnahmen in bestimmten Programmteilen verantwortlich. Außer­halb seines Bereichs erzeugte Ausnahmen interessieren ihn nicht. Sie werden sehen, dass in C++ die Bereiche eines oder mehrerer Ausnahme-Behandler ziemlich deut­lich erkennbar sind. Dabei ist aber nicht ausgeschlossen, dass sich Bereiche verschie­dener Behandler gegenseitig einschließen; bei einer entsprechenden Ausnahme wird dann der Behandler ausgewählt, welcher dem Programmteil, der die Ausnahme er­zeugt hat, am dichtesten ist.

Wichtig ist zu verstehen, dass in C++ das Erkennen einer Ausnahmesituation und die Behandlung derselben an zwei verschiedenen Stellen des Programms geschehen. Das resultiert aus der Beobachtung, dass der erkennende Programmteil häufig gar nicht weiß, wie die Fehlersituation am besten zu behandeln ist. Deshalb „gibt dieser Pro­grammteil auf“ und „hofft“ darauf, dass irgendwo ein Ausnahme-Behandler existiert, der sich der Ausnahme annimmt. Der Behandler hingegen weiß nicht, wo die Aus­nahme genau ausgelöst wurde. Ist das Ausnahme-Objekt hingegen informativ genug (d. h. wurden beim Erzeugen der Ausnahme genügend Informationen im Objekt ge­speichert), kann der Ausnahme-Behandler viel über den Kontext und die Ursache der Ausnahmesituation herausfinden und geeignet reagieren. Dabei müssen Informatio­nen nicht nur als Daten im Objekt enthalten sein: Häufig reicht schon allein der kon­krete Typ des Objekts aus, um eine Ausnahme geeignet zu klassifizieren.

Schließlich ist noch anzumerken, dass es in C++ nicht möglich ist, den Programmab­lauf an jener Stelle fortzusetzen, an welcher der Fehler erkannt wurde. Wenn eine Ausnahme ausgeworfen wird, gibt es keinen Weg zurück: Der Behandler kann viel­leicht ein Problem beheben und den entsprechenden Programmteil, in dem die Feh­lersituation erkannt wurde, erneut ausführen. Er kann aber nicht an derselben Stelle aufsetzen.

5.2 Erzeugen von AusnahmenSetzen wir das Beispiel des letzten Abschnitts in ein einfaches C++-Programm ohne Eingabe-Überprüfung um:

1 /*** Beispiel except1.cpp ***/2 #include <istream>3 #include <ostream>

204

das C++-Modell zur Ausnahmebe­handlung

Begrenzung von Behandlern

Erkennen und Be­handeln von Feh­lern getrennt

kein Wiederauf­setzen nach einer Ausnahme in C++

Page 213: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Fehler erkennen und behandeln

4 #include <iostream>5 using namespace std;67 // teilt „dividend“ durch „divisor“ und gibt das Ergebnis zurück8 int teile (int dividend, int divisor)9 {10 return dividend / divisor;11 }1213 int main ()14 {15 int dividend = 0;16 int divisor = 0;17 cout << "Bitte Dividend eingeben: "; cin >> dividend;18 cout << "Bitte Divisor eingeben: "; cin >> divisor;19 cout20 << dividend << "/" << divisor << " = "21 << teile (dividend, divisor) << endl;22 return 0;23 }

Wir wollen das Programm nun um eine angemessene Fehlerbehandlung erweitern. Ziel ist es, dass das Programm nicht mehr unkontrolliert abbricht, wenn als Divisor Null eingegeben wird.

Als erstes lokalisieren wir die Mögliche Quelle einer Störung. Es handelt sich um die Division in Zeile 10. Die Funktion muss also vor dieser Division prüfen, ob der Divi­sor Null ist, und eine geeignete Ausnahme erzeugen.

Als nächstes müssen wir uns überlegen, welche Informationen wir einem potentiellen Ausnahme-Behandler mitgeben wollen. Wir wollen den Namen der Funktion mitge­ben, in der die Fehlersituation entdeckt wird.

Jetzt haben wir bereits genügend viele Informationen, um ein Ausnahme-Objekt zu beschreiben, die Prüfung zu implementieren und das Auswerfen der Ausnahme zu erledigen:

1 /*** Beispiel except2.cpp ***/2 #include <ostream>3 #include <iostream>4 #include <string>5 #include <exception> // enthält exception-Klasse6 using namespace std;78 // Klasse für „Division durch Null“-Ausnahmen9 class DivisionDurchNull : public exception10 {11 public :12 DivisionDurchNull (const string &function);13 virtual const char *what () const throw ();14 private :15 string m_message;16 };1718 DivisionDurchNull:: DivisionDurchNull (const string &function)19 :20 m_message (21 "Division durch Null in Funktion " + function22 + " aufgetreten!"23 )

205

Wo müssen Aus­nahmen erzeugt werden?

Welche Informa­tionen soll eine Ausnahme mit sich führen?

Page 214: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Fehler erkennen und behandeln Objektorientiertes C++ für Einsteiger

24 {25 }2627 const char *DivisionDurchNull::what () const throw ()28 {29 // konvertiere string-Objekt in eine C-Zeichenkette30 return m_message.c_str ();31 }3233 // teilt „dividend“ durch „divisor“ und gibt das Ergebnis zurück;34 // wirft eine Ausnahme vom Typ „DivisionDurchNull“ aus, wenn der Divisor Null ist35 int teile (int dividend, int divisor)36 {37 if (divisor == 0)38 throw DivisionDurchNull ("teile");39 else40 return dividend / divisor;41 }42

Schauen wir uns die wichtigen Neuerungen im Programm an:

• Zeile 5: Hier benutzen wir die Standard-Header-Datei <exception>, in der die Klasse std::exception definiert ist. Diese Klasse nutzen wir in der Klasse unseres Ausnahme-Objekts als Basisklasse (siehe unten).

Anders als in Java ist es nicht erforderlich, eigene Ausnahme-Klassen von exception (oder einer anderen Klasse) abzuleiten. Genau genommen muss es nicht einmal eine Klasse sein: ein throw-Ausdruck (s. u.) kann auch von einem primitiven Datentyp (etwa int) sein. Es bietet sich aber an, von exception (oder einer spezielleren Klas­se) abzuleiten, da diese Klasse garantiert, dass jedes Ausnahme-Objekt eine Meldung besitzt, die beispielsweise angezeigt und gespeichert werden kann.

• Zeilen 8-16: Hier definieren wir eine Klasse für unsere Ausnahme-Objekte. Sie besitzt einen Konstruktor (definiert ab Zeile 18), der den Namen der die Ausnah­me erzeugenden Funktion entgegennimmt, und eine polymorphe Operation what (definiert ab Zeile 27). Diese Operation ist eine Redefinition der entspre­chenden Operation in der Klasse exception, von der unsere Ausnahme-Klas­se erbt, und ist dafür gedacht, eine Meldung über die ausgeworfene Ausnahme zurückzugeben.

Der Rückgabetyp dieser Methode ist const char *, d. h. ein Zeiger auf ein (Feld von) Zeichen. Es handelt sich dabei um ein C-Relikt: In C gab es keinen Datentyp für Zeichenketten, also behalf man sich mit Feldern aus Zeichen, auf die dann mit Zeigern verwiesen wurde und deren Ende mit einem speziellen Ende-Zeichen ('\0') markiert wurde. Die Operation exception::what hat sehr alte Wurzeln in der C++-Standardisierung, weshalb hier noch diese alte Art verwendet wird, auf Zeichenketten zu verweisen. Es ist jedoch kein Problem, ein string-Objekt in einen solchen Zeiger auf ein Zeichen-Feld umzuwandeln (siehe unten).

Der throw-Modifizierer wird in Abschnitt 5.7 erläutert; hier reicht es zu wissen, dass damit garantiert wird, dass die Methode what unter keinen Umständen eine Ausnahme erzeugt. Tut sie es dennoch, führt dies zu einem „harten“ Program­mabbruch.

206

exception-Klasse als Ober­klasse

what-Operation zur Rückgabe der gespeicherten Meldung

Verwendung der exception-Klasse ist nicht erforderlich

Page 215: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Fehler erkennen und behandeln

• Zeilen 18-25: Hier wird der Konstruktor definiert, der den übergebenen Funk­tionsnamen verwendet, um ein string-Objekt mit einer geeigneten Meldung zu initialisieren.

• Zeilen 27-31: Hier wird die Methode what der Klasse DivisionDurchNull definiert. Beachten Sie den Aufruf der Operation c_str: Damit wird aus einem string-Objekt eine passende C-Zeichenkette erzeugt, die dann zurückgegeben wird.

• Zeilen 37-40: Die Division ist hier um eine Prüfung des Divisors erweitert wor­den: Ist der Divisor Null, wird ein sogenannter throw-Ausdruck ausgewertet. Ein throw-Ausdruck hat folgende Syntax:

throw Ausdruck

Die Bedeutung des throw-Ausdrucks ist die, dass Ausdruck ausgewertet wird und das Ergebnis als Ausnahme an den passenden Ausnahme-Behandler weiter­gereicht wird. In unserem Beispiel erzeugen wir ein temporäres Objekt (4.5.4) vom Typ DivisionDurchNull und übergeben an den Konstruktor unseren Funktionsnamen.

Beachten Sie, dass wie oben erwähnt das Auswerfen einer Ausnahme mit Hilfe von throw den normalen Programmablauf unterbricht und „übergangslos“ zum passenden Ausnahme-Behandler springt. Deshalb ist es auch kein Problem, dass der erste Zweig der if-Anweisung keine return-Anweisung enthält: Die Funktion kehrt hier nicht auf „normalem“ Wege zum Aufrufer zurück, somit ist auch keine return-Anweisung notwendig. Sie würde sowieso nicht ausgeführt, da alle Anweisungen, die auf einen ausgewerteten throw-Ausdruck folgen, oh­nehin ignoriert werden. Wie der „passende“ Ausnahme-Behandler bestimmt wird, werden Sie weiter unten erfahren.

5.3 Behandeln von AusnahmenBis jetzt haben wir einen möglicher Programm-Defekt durch eine entsprechende Prü­fung und das Werfen einer Ausnahme bei negativem Ausgang ersetzt. Noch haben wir uns aber keine Gedanken gemacht, wo und wie wir diese Ausnahme behandeln. Dazu ist erst einmal zu klären, wie Ausnahmen in C++ überhaupt behandelt werden und wie ein passender Ausnahme-Behandler gefunden wird. Dies wollen wir jetzt nachholen.

In C++ ist das Behandeln von Ausnahmen an einen Block von Anweisungen gebun­den. Durch eine entsprechende Syntax kann dem C++-Übersetzer mitgeteilt werden, dass bestimmte Anweisungen „unter Vorbehalt“ ausgeführt werden: Wenn zur Lauf­zeit bei der Ausführung dieser Anweisungen eine Ausnahme erzeugt wird, kann da­für ein passender Behandler „hinterlegt“ werden. Wir wollen dies am obigen Beispiel demonstrieren:

43 int main ()44 {45 int dividend = 0;46 int divisor = 0;

207

c_str wandelt ein string-Ob­jekt in eine C-Zei­chenkette um

Ausnahmen wer­den durch throw-Aus­drücke erzeugt

Ausnahmen ver­lassen den nor­malen Program­mablauf

Ausnahme-Be­handler „überwa­chen“ Anweisun­gen

Page 216: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Fehler erkennen und behandeln Objektorientiertes C++ für Einsteiger

47 cout << "Bitte Dividend eingeben: "; cin >> dividend;48 cout << "Bitte Divisor eingeben: "; cin >> divisor;49 // versuchen (= to try) wir mal...50 try51 {52 cout53 << dividend << "/" << divisor << " = "54 << teile (dividend, divisor) << endl;55 return 0;56 }57 // was tun, wenn eine DivisionDurchNull-Ausnahme ausgelöst wird?58 catch (const DivisionDurchNull &e)59 {60 cout << "Fehler: " << e.what() << endl;61 return 1;62 }63 }

Betrachten wir den Quelltext etwas genauer:

• Zeilen 49-56: Hier wird mit dem Schlüsselwort try ein „überwachter“ Block von Anweisungen eingeleitet. Innerhalb dieses Blocks werden alle Anweisungen auf Ausnahmen geprüft. Weil diese Prüfung aber effektiv zur Laufzeit durchge­führt wird, bedeutet dies, dass auch alle Anweisungen, die indirekt innerhalb die­ses Blocks stattfinden (etwa die Anweisungen einer Funktion, die innerhalb des Blocks aufgerufen wird), ebenfalls überwacht werden. In unserem Beispiel wird dies deutlich: Der throw-Ausdruck steht lexikalisch außerhalb des try-Blocks. Was aber zählt ist, dass zur Laufzeit die Funktion in Zeile 54 innerhalb des try-Blocks aufgerufen und ausgeführt wird, so dass eventuelle Ausnahmen behandelt werden können.

• Zeilen 57-62: In diesen Zeilen steht ein Ausnahme-Behandler. Ein solcher Be­handler beginnt mit dem Schlüsselwort catch und enthält sowohl die Definiti­on eines Ausnahme-Parameters (in runden Klammern) als auch einen Anwei­sungsblock (in geschweiften Klammern). Zuerst bezieht sich ein so definierter Ausnahme-Behandler nur auf den zugehörigen try-Block; Ausnahmen, die wo­anders aufgerufen werden, können nicht mit diesem Ausnahme-Behandler aufge­fangen werden. Der try-Block ist also der Bereich des Ausnahme-Behandlers.

Die Definition des Ausnahme-Parameters regelt, ob ein Ausnahme-Behandler eine Ausnahme behandeln darf: Das ausgeworfene Objekt muss zum catch-Pa­rameter passen. In unserem Beispiel heißt das, dass nur Ausnahmen behandelt werden können, die durch DivisionDurchNull-Objekte (und Objekte abge­leiteter Klassen) dargestellt sind. Wenn ein Ausnahme-Objekt eines anderen, nicht passenden Typs erzeugt wird, wird dieser Behandler übergangen.

Wenn nun eine passende Ausnahme im überwachten Bereich erzeugt wird, wird der normale Programmablauf unterbrochen und das Programm „springt“ direkt vom throw-Ausdruck in Zeile 38 in den zugehörigen Ausnahme-Behandler in Zeile 59. Ab hier wird das Programm ganz normal fortgeführt. Das heißt, dass nach dem Sprung alle Anweisungen wieder „der Reihe nach“ ausgeführt werden.

208

try schließt die zu überwachen­den Anweisungen ein

catch definiert Ausnahme-Be­handler

throw-Ausdruck muss zum catch-Parame­ter passen

Page 217: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Fehler erkennen und behandeln

In unserem Fall wird die Meldung des Ausnahme-Objekts ausgegeben und das Programm via return verlassen.

Beachten Sie, dass das Ausnahme-Objekt, das im throw-Ausdruck initialisiert wurde, am Ende des entsprechenden Ausnahme-Behandlers zerstört wird. In un­serem Fall geschieht dies in Zeile 62, kurz bevor die Kontrolle wieder an die Laufzeit-Umgebung zurückgegeben wird.

Ein try-Block muss immer mindestens einen catch-Block besitzen; umgekehrt kann ein catch-Block nicht ohne einen try-Block existieren. Die Syntax für dieses Sprachkonstrukt ist wie folgt:

try{

[ Anweisungen ]}catch ( Parameter-Definition ){

[ Anweisungen ]}[ catch ( Parameter-Definition ){

[ Anweisungen ]}[ ... ] ]

Wie Sie sehen, können die Blöcke jeweils leer sein. Leere try-Blöcke sind niemals sinnvoll, weil keine Ausnahmen in ihnen entstehen können und die assoziierten catch-Blöcke folglich nie ausgeführt werden können. Leere catch-Blöcke können in einigen (sehr seltenen) Fällen durchaus sinnvoll sein. Allerdings schreiben Sie Ausnahme-Behandler, um Ausnahmen zu behandeln, d. h. auf Ausnahmen geeignet zu reagieren – und „nichts zu tun“ ist nicht unbedingt eine angemessene Reaktion...

Merksatz 32: Vermeide leere catch-Blöcke!

5.4 Ausnahmen während des ProgrammlaufsNachdem wir jetzt unser Programm syntaktisch um die entsprechenden Konstrukte zur Ausnahmebehandlung erweitert haben, wollen wir uns verdeutlichen, was zur Laufzeit da eigentlich passiert. Wir wollen uns dabei auf den Fall beschränken, dass eine Ausnahmesituation entsteht; die erforderlichen Eingaben des hypothetischen Benutzers seien deshalb 7 und 0.

Die Ausführung des Programms startet in der Funktion main:

209

Ausnahme-Objek­te werden am Ende des Behand­lers zerstört

Syntax von try/catch

Ausnahmen zur Laufzeit

Page 218: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Fehler erkennen und behandeln Objektorientiertes C++ für Einsteiger

43 int main ()44 {

Als erstes werden die lokalen Variablen definiert und initialisiert:45 int dividend = 0;46 int divisor = 0;

Dann wird der Benutzer angewiesen, die Eingabe-Daten einzugeben:47 cout << "Bitte Dividend eingeben: "; cin >> dividend;48 cout << "Bitte Divisor eingeben: "; cin >> divisor;

Nun enthalten die Variablen dividend und divisor die Werte 7 und 0.

Als nächstes wird ein überwachter Anweisungsblock eingeleitet. Wenn in den darin enthaltenen Anweisungen eine Ausnahme auftritt, kann sie in einem anhängigen catch-Block (sofern alles passt) behandelt werden:

49 // versuchen (= to try) wir mal...50 try51 {

Jetzt wird das Ergebnis berechnet und ausgegeben:52 cout53 << dividend << "/" << divisor << " = "54 << teile (dividend, divisor) << endl;

Doch halt! Diese Anweisung ist eine Ausdrucks-Anweisung (3.5.1) und enthält einen Funktionsaufruf als Teilausdruck.50 Das bedeutet, dass die Anweisungen in der Funk­tionsdefinition von teile eingeschoben werden:

35 int teile (int dividend, int divisor)36 {

Alle Anweisungen der Funktion sind jetzt logisch Teil des try-Blocks, auch wenn sie lexikalisch nicht im try Block „stehen“. Es zählt allein die Tatsache, dass die Ausführung der Funktion auf eine Anweisung im try-Block zurückgeführt werden kann.

Als erstes wird in der Funktion geprüft, ob divisor Null ist. Das ist bei uns tat­sächlich der Fall, deshalb wird der ersten Zweig der if-Anweisung gewählt. Dort steht ein throw-Ausdruck, der ein temporäres Objekt (4.5.4) des Typs Division­DurchNull initialisiert und danach auswirft. An dieser Stelle wird der gewöhnliche Programmablauf verlassen; insbesondere werden alle folgenden Anweisungen in der Funktion ignoriert:

37 if (divisor == 0)38 throw DivisionDurchNull ("teile");

Jetzt müssen Sie versuchen, sich in die Situation hinein zu versetzen: Sie sind jetzt auf der Suche nach einem passenden Ausnahme-Behandler. Bildlich gesprochen wandern Sie den ganzen Weg zurück, der Sie zum throw-Ausdruck gebracht hat, bis Sie auf einen try-Block stoßen; wenn dieser einen passenden catch-Block 50) Wie Sie in Abschnitt 7.1.3 sehen werden, ist auch die Benutzung des Operators << in diesem Kon­

text letztlich ein Funktionsaufruf; dieser interessiert uns aber an dieser Stelle nicht, da er keine Ausnahmen wirft, die wir behandeln.

210

das Suchen nach dem passenden Behandler

Page 219: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Fehler erkennen und behandeln

beinhaltet, wird dieser dann ausgeführt. Falls kein passender catch-Block vorliegt, gehen Sie weiter zurück bis zum nächsten try-Block u. s. w.

Falls überhaupt kein try-Block gefunden wird oder kein try-Block einen passen­den catch-Block besitzt, geschehen schreckliche Dinge: Das Programm wird auf der Stelle über die Funktion std::terminate abgebrochen!

In unserem Fall ist der erste und einzige try-Block, den wir auf unserem Rückweg finden, der in Zeile 49. Wir müssen nun als nächstes seine catch-Blöcke inspizie­ren, und zwar von oben nach unten (die Reihenfolge ist relevant, siehe Abschnitt 5.6!) Bei jedem der catch-Blöcke überprüfen wir, ob unser throw-Ausdruck zum catch-Parameter passt. Dabei heißt „passend“, dass

(1) die Parameter- und Argument-Typen entweder exakt dieselben sind, oder

(2) die Parameter- und Argument-Typen dieselben sind außer der Tatsache, dass der Parameter-Typ eine Referenz ist und der Argument-Typ nicht, oder

(3) der Parameter-Typ const ist und der Argument-Typ nicht, oder

(4) der Parameter-Typ eine Oberklasse des Argument-Typs ist, oder

(5) der Parameter-Typ ein Zeiger auf eine Oberklasse und der Argument-Typ ein Zeiger auf eine entsprechende Unterklasse ist, oder

(6) der Parameter-Typ eine Referenz auf eine Oberklasse und der Argument-Typ eine Referenz auf eine entsprechende Unterklasse ist.

Die Regeln können auch kombiniert werden: Somit kann ein throw-Argument des Typs Derived von einem catch-Parameter des Typs const Base & aufgefan­gen werden (Kombination der Regeln 2, 3 und 6), vorausgesetzt der Typ Derived ist spezieller als der Typ Base.

Achtung: Bei der Überprüfung auf Typ-Verträglichkeit zwischen throw-Argument und catch-Parameter werden nicht die üblichen Regeln zur Typ-Verträglichkeit ange­wandt, wie sie in Abschnitt 3.4.5 beschrieben sind! Dies hat technische Gründe, auf die wir hier nicht näher eingehen können. Insbesondere werden alle Standard-Konvertierun­gen (außer der Unterklasse-zu-Oberklasse-Konvertierung, die oben erwähnt wurde) nicht durchgeführt. Das kann insbesondere im Bereich primitiver Datentypen zu Über­raschungen führen: Ein throw-Ausdruck, dessen Argument vom Typ int ist (etwa throw 1) wird nicht von einem catch-Ausnahme-Behandler aufgefangen, dessen Parameter vom Typ long ist!

Es gibt in unserem Beispiel jedoch nur einen catch-Block, so dass wir hier nur überprüfen müssen, ob das Argument zum Parameter passt. Unser Argument ist vom Typ DivisionDurchNull, der Parameter vom Typ const Division­DurchNull &. Gemäß den obigen Regeln ist dies in Ordnung. Der Parameter e wird nun mit dem throw-Ausdruck initialisiert. Ab diesem Zeitpunkt gilt die Aus­nahme als behandelt, und der normale Programmablauf ist wiederhergestellt. Es wer­den nun also die Anweisungen im catch-Block ausgeführt:

58 catch (const DivisionDurchNull &e)59 {60 cout << "Fehler: " << e.what() << endl;

211

std::termi­nate

Wann passt ein throw-Ausdruck zu einem catch-Parameter?

Typ-Verträglich­keit ist bei Aus­nahmen anders!

Page 220: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Fehler erkennen und behandeln Objektorientiertes C++ für Einsteiger

61 return 1;62 }

Wenn Sie sich fragen, wie es sein kann, dass ein throw-Ausdruck vom Typ X an einen catch-Parameter von Typ X & (Regel 2) gebunden werden kann, wo doch temporäre Objekte normalerweise nur an const-Referenzen gebunden werden können (4.5.4), ha­ben Sie ein gutes Gespür für „merkwürdige“ Dinge. Die Wahrheit ist, dass die Laufzeit-Umgebung bei der Auslösung einer Ausnahme das Ausnahme-Objekt in einen anderen Speicherbereich kopiert. Dies ist notwendig, weil ein temporäres Objekt am Ende des enthaltenden Ausdrucks zerstört wird; wenn das Ausnahme-Objekt nicht kopiert würde, überlebte ein Ausnahme-Objekt nicht einmal den throw-Ausdruck.51 Das kopierte Ob­jekt gilt dann im Folgenden nicht mehr als temporäres Objekt im eigentlichen Sinn, des­halb ist das Fangen per Nicht-const-Referenz erlaubt. Die Lebensdauer eines Ausnah­me-Objekts erstreckt sich vom Zeitpunkt seiner Erzeugung bis hin zum Verlassen des catch-Blocks, der die Ausnahme behandelt.

Das Kopieren und Zerstören von Ausnahme-Objekten durch die Laufzeit-Umgebung ist übrigens der Grund dafür, dass nur Ausnahme-Objekte benutzt werden können, deren Typ entweder ein primitiver Datentyp ist oder deren Klasse einen öffentlich zugängli­chen Kopierkonstruktor und Destruktor besitzt.

5.5 Behandeln beliebiger AusnahmenIn C++ kann man einem Ausnahme-Behandler erlauben, beliebige Ausnahmen zu behandeln, ohne einen konkreten Typ anzugeben. Statt der üblichen Parameter-Defi­nition wird die Ellipse (...) verwendet. Beispiel:

1 try2 {3 // beliebige Anweisungen4 }5 catch (...) // behandelt alle Ausnahmen, die im obigen try-Block auftreten6 {7 // behandelnde Anweisungen8 }

Das Sprachkonstrukt ist jedoch nicht annähernd so nützlich, wie es vielleicht im ers­ten Moment erscheint. Es liegen dem Behandler nämlich keinerlei Informationen über die aufgefangene Ausnahme vor, so dass die korrekte Behandlung schwierig werden kann. Deshalb wird das Konstrukt typischerweise nur in zwei Situationen verwendet:

(1) im Einsprungspunkt des Programms, der Funktion main, um zu verhindern, dass irgendwelche Ausnahmen das Programm gänzlich verlassen und damit das Pro­gramm über std::terminate außerplanmäßig abgebrochen wird

(2) um Aufräumarbeiten unabhängig von der Ausnahme durchzuführen und danach die Ausnahme weiterzureichen (siehe Abschnitt 5.6)

Das Problem in (2) kann jedoch viel einfacher mit dem RAII-Idiom gelöst werden (siehe hierzu die Abschnitte 4.5.2 und 5.9). Deshalb bleibt nur (1) als Einsatzgebiet des „universellen Ausnahme-Behandlers“, und das ist nicht besonders viel...

51) Diese Erläuterung berücksichtigt nicht, dass der C++-Standard durchaus auch eine direkte Initiali­sierung des passenden catch-Parameters mit dem throw-Argument erlaubt, so dass ein tempo­räres Objekt überflüssig wird. De facto ist mir jedoch keine einzige Implementierung bekannt, die eine derartige Optimierung anbietet.

212

Lebensdauer von Ausnahme Objek­ten

öffentlicher Ko­pierkonstruktor und Destruktor notwendig

der „universelle“ Behandler

Probleme mit catch(...)

Page 221: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Fehler erkennen und behandeln

Ein konkretes Beispiel für die Anwendung eines solchen universellen Ausnahme-Be­handlers finden Sie im nächsten Abschnitt.

5.6 Erneutes Auswerfen von AusnahmenEs kann vorkommen, dass ein Ausnahme-Behandler feststellt, dass er nicht in der Lage ist, die Ausnahme komplett zu behandeln. Dies kann zum Beispiel der Fall sein, wenn der „Behandler“ die Ausnahme gar nicht behandeln, sonder nur in einer Protokoll-Datei vermerken will. In einem solchen Fall hat er die Möglichkeit, die Ausnahme an den nächsten Behandler weiterzureichen. Dazu benutzt er einfach den throw-Ausdruck ohne ein Argument. Beispiel:

1 try2 {3 try4 {5 throw 42; // werfe Ausnahme aus6 }7 catch (...) // behandelt alle Ausnahmen, die im obigen try-Block auftreten8 {9 cout << "Ausnahme aufgefangen!" << endl;10 throw;11 }12 }13 catch (int i)14 {15 cout << "int-Ausnahme aufgefangen: Wert=" << i << endl;16 }

In diesem kleinen Beispiel haben wir zwei geschachtelte try/catch-Blöcke. Im in­neren try-Block wird eine Ausnahme vom Typ int ausgeworfen. Der innere catch-Block bekommt diese als erster zu Gesicht, gibt eine Meldung aus und reicht die Ausnahme an den nächsten Behandler weiter. Der äußere catch-Block be­kommt die weitergereichte Ausnahme als nächster zu sehen, gibt ebenfalls eine (et­was informativere) Meldung aus, reicht die Ausnahme jedoch nicht weiter, so dass mit dem Ende des catch-Blockes der normale Programmablauf wiederhergestellt ist.

Sie sehen an diesem Beispiel auch, warum eine eigene throw-Syntax für das Wei­terreichen der Ausnahme erforderlich ist: Nicht immer haben Sie nämlich Zugriff auf das originale Ausnahme-Objekt, etwa wenn Sie in einem Behandler mit Ellipse (5.5) sind. Aber es gibt auch einen anderen wichtigen Grund, auch bei gewöhnlichen Aus­nahme-Behandlern diese neue Syntax zu verwenden: Slicing. Wenn der Parameter ei­nes catch-Blockes vom Typ der Basisklasse eines Ausnahme-Objekts ist, wird beim Auffangen einer entsprechenden Ausnahme das ursprüngliche Objekt beim In­itialisieren des Parameters abgeschnitten (siehe Abschnitt 4.6.4.1). Wenn Sie dieses Objekt dann mit Hilfe der normalen Syntax weiterreichen, „vergisst“ das Programm den ursprünglichen (spezielleren) Typ der Ausnahme, weil das Parameter-Objekt ja jetzt einen anderen Typ hat. Beispiel:

1 /*** Beispiel rethrow1.cpp ***/2 #include <istream>3 #include <ostream>

213

Weiterreichen von Ausnahmen

throw ohne Ar­gumente ist nötig zum korrekten Weiterreichen

Page 222: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Fehler erkennen und behandeln Objektorientiertes C++ für Einsteiger

4 #include <iostream>5 #include <exception>6 using namespace std;78 class DivisionDurchNull : public exception9 {

10 public :11 virtual const char *what () const throw ();12 };13 const char *DivisionDurchNull::what () const throw ()14 {15 return "Division durch Null!";16 }1718 int teile (int dividend, int divisor)19 {20 if (divisor == 0)21 throw DivisionDurchNull ();22 return dividend / divisor;23 }2425 int berechne (int operand1, int operand2, int f (int, int))26 {27 try28 {29 return f (operand1, operand2);30 }31 catch (exception e) // Achtung: Slicing möglich!32 {33 cout34 << "Ausnahme bei Berechnung aufgetreten: "35 << e.what ()36 << endl;37 throw e; // Achtung: Abgeschnittenes Objekt wird propagiert!38 }39 }4041 int main ()42 {43 int dividend = 0;44 int divisor = 0;45 cout << "Bitte Dividend eingeben: "; cin >> dividend;46 cout << "Bitte Divisor eingeben: "; cin >> divisor;47 try48 {49 int ergebnis = berechne (dividend, divisor, teile);50 cout51 << dividend << "/" << divisor << " = "52 << ergebnis << endl;53 return 0;54 }55 catch (const DivisionDurchNull &e)56 {57 cout << "Division durch Null (Meldung: "58 << e.what () << ")" << endl;59 return 1;60 }61 catch (const exception &e)62 {63 cout << "Ausnahme aufgetreten: " << e.what () << endl;64 return 2;65 }

214

Page 223: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Fehler erkennen und behandeln

66 catch (...)67 {68 cout << "Unbekannte Ausnahme aufgetreten!" << endl;69 return 3;70 }71 }

Das Beispiel enthält gleich zwei Probleme:

(1) Zum einen wird in Zeile 31 die Ausnahme nicht per Referenz aufgefangen. Das erfordert eine zusätzliche Kopie. Schlimmer noch ist, dass auch Objekte abgelei­teter Typen mit diesem Ausnahme-Behandler aufgefangen werden können, etwa ein Objekt des Typs DivisionDurchNull. Da der Typ des Parameters je­doch kein Referenz-Typ ist, wird das ursprüngliche Objekt bei der Initialisierung des catch-Parameters abgeschnitten (4.6.4.1). Das äußert sich darin, dass beim Aufruf der Operation what in Zeile 35 nicht die Methode what der Klasse Di­visionDurchNull ausgeführt und somit keine oder eine falsche Meldung ausgegeben wird (je nachdem, wie exception::what implementiert ist).

(2) Zum anderen wird in Zeile 37 das Ausnahme-Objekt nicht via throw; weiter­gereicht, sondern als Argument explizit angegeben. In diesem Fall heißt das aber, dass das abgeschnittene Objekt weitergereicht wird. Somit wird im weiteren Ver­lauf der Ausnahme-Behandler ausgeführt, der in Zeile 61 und nicht in Zeile 55 beginnt, weil das weitergereichte Objekt nicht (mehr) vom Typ Division­DurchNull ist. Das ist aber garantiert unbeabsichtigt.

Diese beiden Probleme führen dazu, dass die Ausgabe nicht die erwartete ist.Unter VC++ gibt das Programm bei der Eingabe von 5 und 0 als Dividend bzw. Divisor

Bitte Dividend eingeben: 5Bitte Divisor eingeben: 0Ausnahme bei Berechnung aufgetreten: Unknown exceptionAusnahme aufgetreten: Unknown exception

aus. Die Meldung Unknown exception ist eine Standard-Meldung der excepti­on-Klasse unter VC++. Ihre C++-Implementierung kann durchaus eine völlig andere Meldung zurückgeben.

Schließlich können Sie an diesem Beispiel erkennen, dass die Reihenfolge der catch-Blöcke wichtig ist. Wenn Sie die Ausnahme-Behandler in den Zeilen 55-60 und 61-65 vertauschen, wird immer der exception-Ausnahme-Behandler aufgeru­fen, und der speziellere kommt nicht mehr zum Zuge. Das liegt daran, dass Ausnah­me-Behandler in der Reihenfolge ihres Auftretens im Quelltext auf Typ-Verträglich­keit des throw-Arguments mit dem catch-Parameter geprüft werden. Deshalb müssen Sie aufpassen, dass Sie den (oder die) speziellsten Ausnahme-Behandler im­mer zuerst erwähnen.

Merksatz 33: Nutze Referenzen bei Ausnahme-Parametern!

Merksatz 34: Ordne Ausnahme-Behandler vom speziellsten zum allgemeinsten!

Korrigieren Sie nun die beiden aufgezeigten Probleme in Zeile 31:

215

Ausnahmen und Slicing

der Typ des Aus­nahme-Objekts muss erhalten bleiben

Die Reihenfolge von catch-Blö­cken ist wichtig!

Page 224: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Fehler erkennen und behandeln Objektorientiertes C++ für Einsteiger

31 catch (const exception &e) // kein Slicing mehr möglich

und Zeile 37:37 throw; // originalesObjekt wird propagiert

und lassen Sie das Programm ablaufen. Bei einer Eingabe von 5 für den Divisor und 0 für den Dividenden sollte nun die folgende Ausgabe entstehen:

Bitte Dividend eingeben: 5Bitte Divisor eingeben: 0Ausnahme bei Berechnung aufgetreten: Division durch Null!Division durch Null (Meldung: Division durch Null!)

5.7 Ausnahme-SpezifikationenBisher haben wir noch nicht die Rolle des throw-Modifizierers (nicht des throw-Ausdrucks) besprochen. Wenn eine Operation, eine Methode oder eine Funktion am Ende des Kopfes

throw ( [Ausnahmetyp1 [, Ausnahmetyp2 [, ...]]] )enthält, bedeutet dies, dass diese Operation/Methode/Funktion (der Einfachheit hal­ber wird im Rest dieses Abschnitts der Begriff „Funktion“ benutzt) nur Ausnahmen der aufgelisteten (und spezielleren) Typen auswerfen kann. Diese Liste wird Ausnah­me-Spezifikation genannt. Ist diese Liste leer, garantiert die Funktion, dass sie über­haupt keine Ausnahmen auswirft. Wohlgemerkt bedeutet dies nicht, dass innerhalb der Funktion keine Ausnahmen erzeugt werden können; es bedeutet nur, dass keine dieser Ausnahmen die Funktion jemals verlassen werden. Das bedeutet in der Praxis, dass die Funktion entweder ganz simpel ist (so dass einleuchtend ist, dass keine Aus­nahmen auftreten können) oder einen geeigneten try/catch-Block enthält, um die­ses Versprechen halten zu können.

Diese Garantie steht nicht nur „auf dem Papier“ zu Dokumentationszwecken, son­dern wird zur Laufzeit auch rigoros überprüft. Wirft eine Funktion eine Ausnahme aus, die nicht von einem der aufgelisteten Typen ist (und auch nicht von einem spezi­elleren Typ), so reagiert die Laufzeit-Umgebung, indem die Funktion std::unex­pected aufgerufen wird. Diese ruft standardmäßig die Funktion std::termi­nate auf, die einen Programmabbruch bewirkt. Somit wird sichergestellt, dass das Nicht-Einhalten der Ausnahme-Spezifikation nicht zu fatalen Fehlern innerhalb der Anwendung führt, sondern der instabile Programmzustand schnellstmöglich verlas­sen wird.

Java-Programmierer aufgepasst: In C++ lautet das Schlüsselwort throw und nicht throws! Außerdem sind die Ausnahme-Typen in C++ innerhalb runder Klammern aufgelistet, die in Java fehlen.

Weil es sich aber um einen „harten“ Programmabbruch handelt und dieser in vielen Fällen dennoch unerwünscht ist – vielleicht möchte der Entwickler solche Ereignisse protokollieren, den externen Programm-Zustand einigermaßen wieder in Ordnung bringen (etwa temporäre Dateien löschen) u. s. w. – empfehlen viele Autoren, auf die Benutzung von Ausnahme-Spezifikationen zu verzichten. Ein weiterer Grund für diese Empfehlung ist, dass die Prüfung von Ausnahme-Spezifikationen wirklich nur

216

Syntax und Be­deutung von Aus­nahme-Spezifika­tionen

Ausnahme-Spezi­fikationen werden zur Laufzeit über­prüft

Ausnahme-Spezi­fikationen sollten mit Bedacht ver­wendet werden

Page 225: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Fehler erkennen und behandeln

zur Laufzeit und nicht zur Übersetzungszeit passiert. Ein Beispiel verdeutlicht, warum dies so ist:

1 // kapselt das Auftreten eines negativen Arguments2 class NegativeArgument {};34 // wirft eine Ausnahme vom Typ NegativeArgument aus, falls i < 05 void f (int i) throw (NegativeArgument);67 void g () throw ()8 {9 // f() könnte theoretisch eine Ausnahme werfen, tatsächlich passiert dies nie10 f (1);11 }

Der Autor der Funktion f möchte den Benutzer darauf hinweisen, dass bei negativen Werten eine Ausnahme vom Typ NegativeArgument ausgeworfen wird. C++ er­laubt jedoch nicht, diese semantischen Bedingungen syntaktisch zu erfassen. Somit hat der Entwickler nur die Möglichkeit, über eine Ausnahme-Spezifikation generell die Möglichkeit einer NegativeArgument-Ausnahme anzuzeigen und die seman­tischen Bedingungen, die zu dieser Ausnahme führen, im Kommentar zu hinterlegen. Die Funktion g hingegen nutzt f in einer Weise, die gemäß f’s Schnittstelle nicht zu einer Ausnahme führen sollte. Deshalb – und weil keine anderen Anweisungen zu Ausnahmen führen können – ist g’s Ausnahme-Spezifikation leer.

Prüfte bereits der C++-Übersetzer die Ausnahme-Spezifikationen, müsste er mangels tieferer Einsicht in die Kommentare und Implementierung von f deren Aufruf inner­halb von g verbieten. Dies sehen aber viele C++-Entwickler als „Beschneidung ihrer Mündigkeit“ an, weshalb C++-Ausnahme-Spezifikationen nur zur Laufzeit geprüft werden. Für die wirklich unerwarteten Fälle (d. h. wenn Ausnahmen auftreten, die gar nicht auftreten dürfen) gibt es dementsprechend auch eine scharfe Ächtung via std::unexpected seitens der C++-Laufzeit-Umgebung.52

Java sieht seine Entwickler offensichtlich als „weniger mündig“ an, da in Java eben sol­che Ausnahme-Spezifikationen bereits zur Übersetzungszeit geprüft werden. Dies führt dann entweder dazu, dass Ausnahmen behandelt werden müssen, die eigentlich gar nicht auftreten können, oder zu sogenannten Error-Ausnahmen, die diejenige Unter­menge aller Ausnahmen darstellen, die nicht zur Übersetzungszeit überprüft werden.53 Natürlich kann weder dem einen noch dem anderen Ansatz ein absoluter Anspruch auf Angemessenheit und Korrektheit unterstellt werden. Entwickler jedoch, die in beiden Welten entwickeln (müssen), sollten sich der Unterschiede durchaus bewusst sein, wo­bei der Übergang von C++ zu Java typischerweise mit mehr Ärger über den peniblen Übersetzer, der Übergang von Java zu C++ mit mehr Fehlern zur Laufzeit einhergeht...

Merksatz 35: Benutze Ausnahme-Spezifikationen mit Bedacht!

52) Weitere Probleme mit dem Prüfen von Ausnahme-Spezifikationen zur Übersetzungszeit können Sie in [Ellis90], Abschnitt 15.5, nachlesen.

53) Auch RuntimeException-Ausnahmen (und entsprechende Unterklassen) werden nicht ge­prüft.

217

keine statische Prüfung von Aus­nahme-Spezifika­tionen in C++

Page 226: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Fehler erkennen und behandeln Objektorientiertes C++ für Einsteiger

Bisher haben wir keine Ausnahme-Spezifikationen verwendet. Die Frage stellt sich, welcher Ausnahme-Spezifikation dies entspricht. Die Antwort ist nicht offensicht­lich: Eine Funktion ohne Ausnahme-Spezifikation sagt nicht aus, dass sie keine Aus­nahmen auswirft, sondern dass sie beliebige Ausnahmen auswerfen kann. Ein Grund dafür ist, dass es keine throw-Syntax für den Fall „kann alles Mögliche auswerfen“ gibt.

Auch das ist etwas, was sich von den Ausnahme-Spezifikationen in Java unterscheidet. In Java bedeutet bekanntlich das Fehlen der Ausnahme-Spezifikation die Garantie, dass keine Ausnahmen ausgeworfen werden. Und der Fall, dass eine Methode jede beliebige Ausnahme auswerfen kann, kann mit Hilfe von throws Exception ausgedrückt werden. Das geht in C++ so nicht, weil nicht gefordert ist, dass Ausnahme-Typen von einer bestimmten Klasse abgeleitet sind.

Die Kombination von Ausnahme-Spezifikationen und Spezialisierung erfordert zu­sätzlich etwas Nachdenken. Wir wollen, dass das Liskov’sche Substitutionsprinzip (4.1.6, 4.6.4.1) auch im Kontext von Ausnahme-Spezifikationen gilt. Der Schlüssel hierfür ist, Ausnahmen gewissermaßen als spezielle Nachbedingungen zu interpretie­ren. Wenn also eine Operation in einer Basisklasse garantiert, nur eine bestimmte Menge an Ausnahmen jemals auszuwerfen, dann darf eine implementierende Metho­de in einer abgeleiteten Klasse nicht mehr Ausnahmen erlauben. Beispiel:

1 #include <exception>2 using namespace std;34 // Ausnahme-Klasse für ungültige Argumente5 class UngueltigesArgument : public exception6 {7 public :8 virtual const char *what () const throw ();9 };

10 // Ausnahme-Klasse für Datei-Ein-/Ausgabe-Fehler11 class DateiFehler : public exception12 {13 public :14 virtual const char *what () const throw ();15 };1617 // Fakultät-Dienst18 class Fakultaet19 {20 public :21 // Berechnet die Fakultät von i. i muss positiv sein, ansonsten wird eine22 // Ausnahme vom Typ UngueltigesArgument ausgeworfen.23 virtual int berechne (int i)24 throw (UngueltigesArgument);25 };2627 class FakultaetMitDateiausgabe : public Fakultaet28 {29 public :30 // Berechnet die Fakultät von i und speichert den errechneten Wert in einer Datei ab.31 // Wenn das Schreiben in die Datei fehlschlägt, wird eine Ausnahme vom Typ DateiFehler32 // ausgeworfen.33 virtual int berechne (int i) // Redefinition34 throw (UngueltigesArgument, DateiFehler);35 };

218

fehlende Ausnah­me-Spezifikation

Ausnahme-Spezi­fikationen und Spezialisierung

Page 227: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Fehler erkennen und behandeln

In diesem Beispiel ist der Quelltext der Methoden der Übersichtlichkeit halber weg­gelassen worden. Er spielt aber auch keine Rolle in unseren Betrachtungen.

Schauen wir uns einmal die Methode berechne in beiden Klassen an. In der Basis­klasse Fakultaet enthält die Ausnahme-Spezifikation die Ausnahme-Klasse Un­gueltigesArgument, in der abgeleiteten Klasse FakultaetMitDateiaus­gabe zusätzlich den Typ DateiFehler. Nun überlegen Sie einmal, wie sich diese erweiterte Ausnahme-Spezifikation in der abgeleiteten Klasse auf Klienten auswirkt, die bisher nur mit Objekten der Basisklasse Fakultaet gearbeitet haben. Sicher­lich gehen diese davon aus, dass nur Ausnahmen vom Typ UngueltigesArgu­ment ausgeworfen werden. Vielleicht behandeln sie diese Ausnahmen mit einem try/catch-Konstrukt ähnlich dem folgenden:

36 // wirft keine Ausnahmen aus!37 void fakultaetKlient (Fakultaet &f, int i) throw ()38 {39 try40 {41 int ergebnis = f.berechne (i);42 cout << i << "! = " << ergebnis << endl;43 }44 catch (const UngueltigesArgument &e)45 {46 cout << "Ausnahme: " << e.what () << endl;47 }48 }

Diese Funktion hat bisher erfolgreich mit Fakultaet-Objekten gearbeitet. Aber was passiert, wenn ein FakultaetMitDateiausgabe-Objekt übergeben wird? Wenn die Dateiausgabe problemlos funktioniert, ist alles in Ordnung. Wenn sie aber fehlschlägt (aus welchen Gründen auch immer), hat fakultaetKlient ein riesi­ges Problem: Die Funktion behandelt die Ausnahme vom Typ DateiFehler nicht. Deshalb wird sie an den nächsten Ausnahme-Behandler weitergereicht. Dadurch aber, dass fakultaetKlient eine leere Ausnahme-Spezifikation besitzt und da­mit effektiv aussagt, keine Ausnahmen an seinen Aufrufer weiterzureichen, wird die C++-Laufzeit-Umgebung das Programm via die unexpected/terminate-Kette auf schnellstem Wege beenden.

Deshalb ist bei der Implementierung von Operationen bzw. der Redefinition von Me­thoden darauf zu achten, dass die Ausnahme-Spezifikation der spezielleren Opera­tion/Methode nicht mehr mögliche Ausnahme-Typen umfasst als diejenige der allge­meineren Operation/Methode. Diese Regel verbietet die obige Redefinition. Sie verbietet auch, dass die what-Methode der Klasse UngueltigesArgument oder der Klasse DateiFehler mit einer nicht leeren Ausnahme-Spezifikation versehen wird. Da exception::what eine leere Ausnahme-Spezifikation besitzt und wir gemäß der Regel in spezielleren Klassen keine Ausnahme-Typen zur Ausnahme-Spezifikation hinzufügen dürfen, muss die Ausnahme-Spezifikation der Methode what in von exception abgeleiteten Klassen leer bleiben.

219

speziellere Me­thoden dürfen nicht mehr Aus­nahmen werfen

Page 228: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Fehler erkennen und behandeln Objektorientiertes C++ für Einsteiger

Beachten Sie, dass der umgekehrte Fall von der Regel nicht verboten wird: Eine spe­ziellere Operation/Methode kann durchaus weniger Ausnahme-Typen in ihrer Aus­nahme-Spezifikation auflisten als die Operation/Methode der allgemeineren Klasse. Denken Sie ruhig darüber nach, und versuchen Sie Beispiele zu finden, um sich die Gültigkeit des Substitutionsprinzips in solchen Fällen klar zu machen.

5.8 Ausnahme-Sicherheit und warum sie so wichtig istIn diesem Abschnitt wollen wir untersuchen, was Ausnahmesicherheit bedeutet und wann ein Programm (oder ein Programm-Teil, etwa eine Methode oder eine Klasse) ausnahmesicher ist. Wir betrachten im Folgenden Funktionen, die gewonnenen Er­kenntnisse können jedoch problemlos auf Methoden, Klassen und ganze Programme ausgeweitet werden. Wir können im Rahmen dieses Skripts dieses Thema nur kurz anschneiden; für eine detaillierte Diskussion ist [Sutt00] sehr zu empfehlen.

Generell gibt es drei Garantien, die eine Funktion ihren Klienten zusichern kann54, wobei jede Garantie die vorherigen Garantien einschließt:

(1) Grundlegende Garantie: Auch in der Gegenwart von Ausnahmen verursacht die Funktion keine Ressourcen-Lecks. Dies bedeutet, dass Ausnahmen nicht zu irre­parablen Fehlern und letztlich zu einem Programmabsturz führen.

(2) Hohe Garantie: Tritt während der Abarbeitung der Funktion eine Ausnahme auf, so stellt die Funktion sicher, dass sich der Zustand des Programms nicht verändert. Das ist eine andere Formulierung des „Ganz-oder-gar-nicht“-Prinzips: Entweder führt die Funktion alle ihre Aufgaben erfolgreich durch oder gar keine. „Halbe“ Erfolge existieren nicht.55

(3) Absolute Garantie (oder „nothrow“-Garantie): Die Funktion wirft unter keinen Umständen eine Ausnahme aus.

Schauen wir uns dazu einige Beispiele an:1 void tuEtwasAnderes (int &i);2 void tuEtwas ()3 {4 int *i = new int (0);5 tuEtwasAnderes (*i);6 delete i;7 }

Die Funktion tuEtwas erfüllt aller Wahrscheinlichkeit nicht einmal die grundle­gende Garantie. Wenn während der Abarbeitung der Funktion tuEtwasAnderes eine Ausnahme auftritt, wird der Speicherbereich der int-Variable, die in Zeile 4 er­zeugt und initialisiert wird, nicht freigegeben. Somit entsteht ein Ressourcen-Leck.

1 void tuEtwasAnderes (int &i);2 void tuEtwas ()3 {4 int i = 0;5 tuEtwasAnderes (i);6 }

54) vgl. [Sutt00]55) Für Leser, die sich mit Transaktionen auskennen: Die hohe Garantie ist vergleichbar mit der For­

derung nach Atomarität (Atomicity) für Transaktionen.

220

speziellere Me­thoden dürfen we­niger Ausnahmen werfen

Ausnahme-Si­cherheit und Aus­nahme-Garantien

die drei Ausnah­me-Garantien

grundlegende Garantie

hohe Garantie

absolute Garantie

Beispiele

Page 229: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Fehler erkennen und behandeln

Die Funktion benutzt jetzt keinen dynamischen Speicher mehr. Jetzt hängt die Ga­rantie, die tuEtwas zusichern kann, von tuEtwasAnderes ab:

• Wenn tuEtwasAnderes die hohe Garantie zusichert, dann sichert tuEtwas diese auch zu. Das liegt daran, das tuEtwas nicht wirklich etwas am Pro­grammzustand ändert, sondern die ganze Arbeit tuEtwasAnderes überlässt.

• Wenn tuEtwasAnderes die absolute Garantie zusichert, dann sichert tuEt­was diese auch zu. Das liegt daran, dass tuEtwas selbst keine Ausnahmen aus­wirft; wenn tuEtwasAnderes dies auch nicht tut, erfüllen beide Funktionen die absolute Garantie.

An diesem Beispiel ist zu erkennen, dass die zugesicherte Ausnahme-Garantie durch­aus von den Garantien benutzter Programm-Teile abhängt.

Schließlich noch ein Beispiel für das Einhalten der grundlegenden (aber nicht hohen) Garantie:

1 void tuEtwasAnderes (int &i);2 void tuEtwas ()3 {4 for (int i = 0; i < 10; ++i)5 tuEtwasAnderes (i);6 }

Hier erfüllt tuEtwas nur die grundlegende Garantie, falls tuEtwasAnderes dies tut und zu beliebiger Zeit eine Ausnahme auswerfen kann. Stellen Sie sich vor, beim sechsten Aufruf (i==6) wirft tuEtwasAnderes eine Ausnahme aus. Dann wer­den die fünf vorherigen und erfolgreichen Aufrufe in ihrer Wirkung aber nicht rück­gängig gemacht. Das Ganz-oder-gar-nicht-Prinzip gilt also nicht. Erfüllt jedoch tu­EtwasAnderes mindestens die grundlegende Garantie, gilt dies auch für tuEtwas.

Wozu untersuchen wir überhaupt diese Garantien? Nun, wenn Sie Ausnahmen in Ih­rem Programm einsetzen – und Einsetzen bedeutet auch, andere Programm-Kompo­nenten zu benutzen, die Ausnahmen einsetzen –, dann müssen Sie sich mit den Ga­rantien herumschlagen. Denn nur wenn alle Ihre Funktionen, Methoden, Klassen etc. die grundlegende Garantie erfüllen, ist Ihr Programm ausnahmesicher. Und Ausnah­mesicherheit ist Ihr wichtigstes Ziel. Denn nur dann haben Sie Gewissheit, dass Ihr Programm auch unter unvorhergesehenen Umständen nicht wichtige Ressourcen ver­liert und den Programmzustand durcheinander bringt.

Die hohe Garantie ist natürlich besser als die grundlegende, allerdings häufig mit ei­nem erhöhten Aufwand verbunden, sowohl in der Implementierung als auch in Ana­lyse und Entwurf. Deshalb ist hier in der Regel eine Kosten-Nutzen-Analyse sehr hilfreich.

Merksatz 36: Stelle zumindest die grundlegende Ausnahme-Garantie sicher!

221

Ausnahme-Ga­rantie ist abhän­gig von benutzen Programm-Teilen

Einhaltung der grundlegenden Ausnahme-Ga­rantie notwendig

Page 230: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Fehler erkennen und behandeln Objektorientiertes C++ für Einsteiger

5.9 Ausnahmen und RAIIAbschnitt 4.5.2 hat bereits angedeutet, dass RAII und Ausnahmesicherheit eine na­türliche Symbiose eingehen. Warum dies so ist, wollen wir in diesem Abschnitt un­tersuchen. Wir schauen uns dazu das Beispiel aus Abschnitt 4.5.2 noch einmal leicht abgewandelt an:

1 /*** Beispiel except_raii.cpp ***/2 #include <ostream>3 #include <iostream>4 #include <string>5 using namespace std;67 // wird ausgeworfen, wenn das Öffnen der Datei fehlschlägt8 class FileOpenException9 {

10 };1112 class File13 {14 public :15 // Konstruktor: öffnet die angegebene Datei16 // wirft eine FileOpenException-Ausnahme aus, wenn das Öffnen fehlschlägt17 File (string name);1819 // Destruktor, schließt die geöffnete Datei20 ~File ();2122 // liest „count“ Zeichen aus der zuvor geöffneten Datei23 string read (int count);2425 // schreibt die Zeichenkette „data“ in die zuvor geöffnete Datei26 void write (string data);2728 private :29 // von der Implementierung benötigte Attribute3031 // öffnet die angegebene Datei; liefert true bei Erfolg und false bei Misserfolg zurück32 bool open (string name);3334 // schließt die zuvor geöffnete Datei35 void close ();36 };3738 File::File (string name)39 {40 // öffne Datei; wenn das fehlschlägt, wirf eine Ausnahme aus41 if (!open (name))42 throw FileOpenException ();43 }4445 File::~File ()46 {47 // schließe die Datei und gib Ressourcen frei48 close ();49 }5051 int main ()52 {53 try

222

Page 231: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Fehler erkennen und behandeln

54 {55 File myFile ("myfile.dat");56 File myFile2 ("myfile2.dat");57 string acht = myFile.read (4) + myFile2.read (4);58 cout << "die ersten 4 Zeichen aus beiden Dateien: "59 << acht << endl;60 // hier endet der Gültigkeitsbereich von myFile und myFile2; dabei werden61 // automatisch die Destruktoren aufgerufen und die geöffneten Dateien geschlossen62 }63 catch (FileOpenException)64 {65 // wenn wir hier ankommen, hat das Öffnen der Datei nicht geklappt66 cout << "Konnte Datei nicht öffnen!" << endl;67 }68 return 0;69 }

Die Änderung findet sich in den Zeilen 55-57: Wo vorher nur ein File-Objekt initi­alisiert wurde, sind es jetzt deren zwei. Diese kleine Änderung hat aber eine große Auswirkung auf die Ausnahmesicherheit, wie wir gleich sehen werden.

Stellen wir uns einmal vor, dass die Datei myfile.dat in Zeile 55 erfolgreich ge­öffnet werden kann. Jetzt versucht unser Programm in Zeile 56, die Datei myfi­le2.dat zu öffnen. Wir wissen, dass eine Ausnahme vom Typ FileOpen­Exception ausgeworfen wird, wenn das Öffnen fehlschlägt. Die Preisfrage ist nun: Was passiert nun mit der bereits geöffneten Datei myfile.dat?

Wenn die Ausnahme vom Konstruktor der Klasse File in Zeile 42 ausgeworfen wird, ist der einzige catch-Block, der als Ausnahme-Behandler in Frage kommt, derjenige in den Zeilen 63-67. Der Gültigkeitsbereich des File-Objekts myFile in Zeile 55 ist jedoch bereits in Zeile 62 zu Ende. Das bedeutet, dass das Objekt myFi­le zum Zeitpunkt der Ausnahme-Behandlung zerstört worden sein muss.

Das Zerstören eines File-Objekts ist jedoch untrennbar mit der Ausführung des File-Destruktors verbunden. Dieser schließt die Datei ordnungsgemäß. Können wir also davon ausgehen, dass die Datei myfile.dat zum Zeitpunkt der Ausnahme-Behandlung in den Zeilen 63-67 korrekt geschlossen worden ist?

Das können wir in der Tat. C++ garantiert, dass alle lokalen Objekte, die durch das Auswerfen und Behandeln einer Ausnahme ihre Gültigkeit verlieren, ordnungsgemäß zerstört werden, d. h. dass für jedes dieser Objekte der zugehörige Destruktor aufge­rufen wird. Die betreffenden Objekte sind dabei alle jene, die seit Beginn des try-Blocks erzeugt und noch nicht zerstört worden sind. In unserem Fall betrifft dies nur das Objekt myFile, das in Zeile 55 erstellt und initialisiert wird. Objekte außerhalb des try-Blocks sind entweder noch gültig (dies wäre für lokale Objekte der Fall, die vor dem Beginn des try-Blocks definiert sind) oder noch nicht gültig (dies gilt für alle lokalen Objekte hinter dem letzten, zum try-Block gehörenden catch-Block).

Sie sehen also, dass das RAII-Idiom Ihnen hilft, die hohe Ausnahme-Garantie zu er­füllen. Denn wenn der Destruktor alles rückgängig macht, was der Konstruktor getan hat, dann werden bei einer Ausnahme alle Effekte zurückgenommen, die zwischen dem Beginn des try-Blocks und dem Auswerfen einer Ausnahme durchgeführt

223

Ausnahmen und lokale Objekte

Ausnahmen zer­stören ungültig gewordene lokale Objekte

Page 232: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Fehler erkennen und behandeln Objektorientiertes C++ für Einsteiger

wurden. Somit erlaubt Ihnen RAII, sich auf den ausnahmesicheren Entwurf Ihrer Klassen mit passenden Konstruktoren und Destruktoren zu konzentrieren; den Rest erledigt der Übersetzer und die Laufzeit-Umgebung.

Nun fragen Sie sich sicherlich, warum die Betonung auf lokalen Objekten liegt. Nun, die Antwort ist einfach: Wenn Sie dynamische Objekte verwenden, werden diese nicht zerstört, was zu Ressourcen-Lecks führt! Betrachten Sie einmal das obige Bei­spiel derart abgewandelt, dass dynamische Objekte und nicht lokale Objekte verwen­det werden:

53 try54 {55 File *myFile = new File ("myfile.dat");56 File *myFile2 = new File ("myfile2.dat");57 string acht = myFile->read (4) + myFile2->read (4);58 cout << "die ersten 4 Zeichen aus beiden Dateien: "59 << acht << endl;60 // hier endet der Gültigkeitsbereich von myFile und myFile261 }62 catch (FileOpenException)63 {64 // wenn wir hier ankommen, hat das Öffnen der Datei nicht geklappt65 cout << "Konnte Datei nicht öffnen!" << endl;66 }

Jetzt werden die Objekte in den Zeilen 55 und 56 über den Operator new im dyna­misch erzeugt. Wo genau sind jetzt die Unterschiede?

Der wichtigste und fatalste ist der, dass die erzeugten Objekte nicht ordnungsgemäß zerstört werden. Woran liegt das? Wenn der Gültigkeitsbereich von myFile und myFile2 in Zeile 61 verlassen wird, werden diese Variablen zerstört. Dummerwei­se sind diese Variablen keine File-Objekte, sondern Zeiger auf File-Objekte! Wenn jedoch ein Zeiger zerstört wird, wird das Objekt „hinter“ dem Zeiger nicht zer­stört. Dies muss auch so sein, schließlich können in einem Programm durchaus meh­rere Zeiger auf dasselbe Objekt verweisen.56 Dies führt jedoch in diesem Fall zu Res­sourcen-Lecks, weil Sie in Ihrem Programm nie mehr an die beiden File-Objekte herankommen können. Ihre Dateien werden somit eventuell nicht korrekt geschlos­sen.57 Und wenn schon ein normaler Programmablauf (d. h. einer ohne Ausnahmen) zu Ressourcen-Lecks führt, sollte klar sein, dass Ausnahmen die Situation nicht gera­de verbessern.

Nun können Sie natürlich Ihren Code so umschreiben, dass explizit der Operator delete aufgerufen wird:

53 try54 {55 File *myFile = new File ("myfile.dat");56 File *myFile2 = new File ("myfile2.dat");57 string acht = myFile->read (4) + myFile2->read (4);58 cout << "die ersten 4 Zeichen aus beiden Dateien: "59 << acht << endl;

56) Hinzu kommt, dass es in C++ keine eingebaute „Garbage Collection“ gibt, die unbenutzte Objekte findet und freigibt.

57) Die meisten Betriebssysteme schließen nach Programmende alle noch offenen Dateien; darauf kann man sich aber in einem portablen C++-Programm nicht verlassen.

224

Benutzung von dynamischen Ob­jekten ist proble­matisch

delete allein hilft nicht!

Page 233: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Fehler erkennen und behandeln

60 delete myFile2;61 delete myFile;62 // hier endet der Gültigkeitsbereich von myFile und myFile263 }64 catch (FileOpenException)65 {66 // wenn wir hier ankommen, hat das Öffnen der Datei nicht geklappt67 cout << "Konnte Datei nicht öffnen!" << endl;68 }

Jetzt funktioniert Ihr Programm im Ausnahme-losen Fall korrekt. Es ist aber nicht ausnahmesicher. Betrachten Sie wieder Zeile 56: Wenn die Initialisierung des dort erzeugten File-Objekts fehlschlägt, wird das Objekt, auf dass myFile verweist (Zeile 55) nie freigegeben, weil der entsprechende delete-Aufruf in Zeile 61 nicht ausgeführt wird. Somit wird nicht einmal die grundlegende Garantie erfüllt.

Natürlich wäre es am Einfachsten, Ihnen zu raten, sich auf lokale Objekte zu be­schränken. Allerdings ist das nicht immer möglich. Deshalb muss es eine weitere Lö­sung für das Problem geben. Und die gibt es in der Tat: Wir müssen nur sicherstel­len, dass das Objekt, auf das ein Zeiger zeigt, bei der Zerstörung des Zeigers auch zerstört wird. Für genau diesen Fall gibt es die Klasse auto_ptr in der C++-Stan­dard-Bibliothek. Diese Klasse ist quasi ein intelligenter Zeiger-Ersatz und stellt in ih­rem Destruktor sicher, dass das Objekt, auf das verwiesen wird, ebenfalls zerstört wird.

Das obige Beispiel schreibt sich nun folgendermaßen:53 try54 {55 auto_ptr<File> myFile (new File ("myfile.dat"));56 auto_ptr<File> myFile2 (new File ("myfile2.dat"));57 string acht = myFile->read (4) + myFile2->read (4);58 cout << "die ersten 4 Zeichen aus beiden Dateien: "59 << acht << endl;60 // hier endet der Gültigkeitsbereich von myFile und myFile2; die Destruktoren der61 // auto_ptr-Klasse kümmern sich um die Zerstörung der assoziierten File-Objekte62 }63 catch (FileOpenException)64 {65 // wenn wir hier ankommen, hat das Öffnen der Datei nicht geklappt66 cout << "Konnte Datei nicht öffnen!" << endl;67 }

Sie sehen in den Zeilen 55 und 56, dass die File-Verweise nicht mehr vom Typ File *, sondern vom Typ auto_ptr<File> sind. Die Klasse auto_ptr ist nämlich gar keine „richtige“ Klasse, sondern eine Schablone (7.2) und als solche für viele Typen anwendbar. Die genaue Syntax können wir hier nicht weiter erläutern; hier ist es nur wichtig, dass dadurch eine Art intelligenter Zeiger auf ein File-Ob­jekt gemeint ist. Weiterhin müssen Sie noch die Header-Datei <memory> einbinden, um auto_ptr verwenden zu können.

Nun ist Ihr Programm ausnahmesicher, denn sowohl bei einem normalen Program­mablauf als auch bei der Existenz von Ausnahmen werden die Destruktoren der File-Objekte ausgeführt und die Ressourcen ordnungsgemäß freigegeben. Zu be­merken ist hier noch abschließend, dass die Ausnahmesicherheit durch die Anwen­

225

intelligente Zei­ger helfen hier

Page 234: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Fehler erkennen und behandeln Objektorientiertes C++ für Einsteiger

dung des RAII-Idoms hergestellt worden ist, und zwar in der auto_ptr-Schablone. RAII ist also immer wieder zum Erreichen von Ausnahmesicherheit nützlich.

Was lernen wir daraus? Ausnahmesicherheit gehört wohlüberlegt; es reicht nicht aus, einfach ein paar try/catch-Blöcke in den Quelltext zu schreiben. Es ist essentiell, sich damit auseinanderzusetzen, welche Ressourcen wo angefordert und wo freigege­ben werden und ob das alles auch ordnungsgemäß funktioniert, wenn Ausnahmen ins Spiel kommen. Generell dürfen Sie davon ausgehen, dass das Schreiben ausnahmesi­cherer Programm sicherlich etwas komplexer ist als das Schreiben von Programmen, die sich nicht um Ausnahmesicherheit scheren. Allerdings erspart Ihnen das RAII-Idiom viel Arbeit und Hirnschmalz, und die Zeit, die Sie in Ausnahmesicherheit in­vestieren, wird der Qualität Ihrer Programme doppelt und dreifach zugute kommen.

Merksatz 37: Verwende RAII in ausnahmesicheren Programmen!

Eine wichtige Sache zum Schluss: Stellen Sie in Ihren Programmen sicher, dass ein De­struktor niemals eine Ausnahme auswirft! Das erschöpfend zu erklären sprengt den Rahmen dieses Skripts58, soviel aber dazu: Wenn eine Ausnahme ausgeworfen, aber noch nicht behandelt wurde, werden die Destruktoren aller ungültig werdender lokaler Objekte aufgerufen (s. o.) Wenn aber während der Ausführung eines solchen Destruk­tors erneut eine Ausnahme ausgeworfen wird, haben wir eine Ausnahmesituation inner­halb einer Ausnahmesituation. Die C++-Sprache fordert, dass in einem solchen Fall das Programm umgehend terminiert wird. Verbannen Sie also unbedingt Ausnahmen aus Destruktoren!

Merksatz 38: Stelle sicher, dass kein Destruktor jemals eine Ausnahme wirft!

58) Sie können Details in [Sutt00] und [Sutt02] nachlesen.

226

RAII und Ausnah­me-Sicherheit ge­hen Hand in Hand

Destruktoren und Ausnahmen

Page 235: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Entwurfsmuster

6 EntwurfsmusterWird in einer späteren Version des Skripts ergänzt

6.1 EinführungWird in einer späteren Version des Skripts ergänzt

6.2 Strukturelle MusterWird in einer späteren Version des Skripts ergänzt

6.2.1 CompositeWird in einer späteren Version des Skripts ergänzt

6.2.2 DecoratorWird in einer späteren Version des Skripts ergänzt

6.2.3 ProxyWird in einer späteren Version des Skripts ergänzt

6.3 VerhaltensmusterWird in einer späteren Version des Skripts ergänzt

6.3.1 Template MethodWird in einer späteren Version des Skripts ergänzt

6.3.2 IteratorWird in einer späteren Version des Skripts ergänzt

6.3.3 ObserverWird in einer späteren Version des Skripts ergänzt

6.3.4 StrategyWird in einer späteren Version des Skripts ergänzt

6.3.5 StateWird in einer späteren Version des Skripts ergänzt

6.3.6 CommandWird in einer späteren Version des Skripts ergänzt

6.4 ErzeugungsmusterWird in einer späteren Version des Skripts ergänzt

227

Page 236: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Entwurfsmuster Objektorientiertes C++ für Einsteiger

6.4.1 Abstract FactoryWird in einer späteren Version des Skripts ergänzt

6.4.2 SingletonWird in einer späteren Version des Skripts ergänzt

228

Page 237: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Überladung und Schablonen

7 Überladung und SchablonenWird in einer späteren Version des Skripts ergänzt

7.1 ÜberladungWird in einer späteren Version des Skripts ergänzt

7.1.1 Überladung von FunktionenWird in einer späteren Version des Skripts ergänzt

7.1.2 Überladung von Operationen und MethodenWird in einer späteren Version des Skripts ergänzt

7.1.3 Überladung von OperatorenWird in einer späteren Version des Skripts ergänzt

7.2 Schablonen (Templates)Wird in einer späteren Version des Skripts ergänzt

229

Page 238: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,
Page 239: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger Die Standard-Bibliothek

8 Die Standard-BibliothekWird in einer späteren Version des Skripts ergänzt

8.1 EinführungWird in einer späteren Version des Skripts ergänzt

8.2 NamensräumeWird in einer späteren Version des Skripts ergänzt

8.3 DatenstrukturenWird in einer späteren Version des Skripts ergänzt

8.4 AlgorithmenWird in einer späteren Version des Skripts ergänzt

8.5 Ein-/AusgabeWird in einer späteren Version des Skripts ergänzt

231

Page 240: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger

MerksätzeMerksatz 1: Jedes C++-Programm ist eine Folge von Deklarationen!............................17Merksatz 2: Kommentiere so gut du kannst!...................................................................21Merksatz 3: Für jede verwendete Entität gibt es genau eine Definition!........................ 28Merksatz 4: Tue Schnittstellen und Implementierung in verschiedene Dateien!............ 31Merksatz 5: Verwende niemals nicht initialisierte Variablen!........................................ 33Merksatz 6: Mach dich möglichst nicht von lokalen Eigenheiten abhängig!..................46Merksatz 7: Vermeide den unkontrollierten Umgang mit Zeigern!................................ 60Merksatz 8: Verwende symbolische Konstanten!........................................................... 63Merksatz 9: Meide gefährliche Typ-Umwandlungen!.................................................... 65Merksatz 10: Verwende Abstraktionen!..........................................................................69Merksatz 11: Verwende ausführliche Funktionskommentare!........................................83Merksatz 12: Überlege gut den Einsatz von rekursiven Funktionen!............................. 91Merksatz 13: Überlege gut den Einsatz von Schleifen!.................................................. 91Merksatz 14: Verwende nie öffentliche Attribute!........................................................122Merksatz 15: Verwende const bei beobachtenden Operationen!.................................. 123Merksatz 16: Teste viel und ausführlich!...................................................................... 126Merksatz 17: Verwende Namensräume zur Modularisierung!......................................130Merksatz 18: Verwende eine einheitliche Reihenfolge bei Attributen!........................ 134Merksatz 19: Betrachte Konstruktoren und Destruktor als Team!................................137Merksatz 20: Verwende Destruktoren zur Ressourcen-Freigabe!.................................142Merksatz 21: Betrachte Kopierkonstruktor und Zuweisungsoperator als Team!..........148Merksatz 22: Vermeide Null-Zeiger, wo es nur geht!...................................................154Merksatz 23: Verwende Polymorphie anstatt Fallunterscheidungen!........................... 170Merksatz 24: Achte bei der Redefinition einer Methode auf Typ-Gleichheit!..............174Merksatz 25: Nutze kovariante Rückgabetypen, um Casts zu vermeiden!................... 175Merksatz 26: Verwende virtuelle Destruktoren bei Basisklassen!................................ 185Merksatz 27: Vermeide den Einsatz virtueller Basisklassen!........................................192Merksatz 28: Einmal virtuelle Basisklasse, immer virtuelle Basisklasse!.................... 192Merksatz 29: Vermeide konkrete virtuelle Basisklassen!............................................. 196Merksatz 30: Korrektheit einer Spezialisierung ist vom Verhalten abhängig!..............201Merksatz 31: Bedenke den Einsatz von Vererbung!..................................................... 201Merksatz 32: Vermeide leere catch-Blöcke!................................................................. 209Merksatz 33: Nutze Referenzen bei Ausnahme-Parametern!........................................215Merksatz 34: Ordne Ausnahme-Behandler vom speziellsten zum allgemeinsten!....... 215Merksatz 35: Benutze Ausnahme-Spezifikationen mit Bedacht!..................................217Merksatz 36: Stelle zumindest die grundlegende Ausnahme-Garantie sicher!............. 221Merksatz 37: Verwende RAII in ausnahmesicheren Programmen!.............................. 226Merksatz 38: Stelle sicher, dass kein Destruktor jemals eine Ausnahme wirft!........... 226

232

Page 241: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger

BeispieleBeispiel "hello"................................................................................................................10Beispiel "kommentar"......................................................................................................20Beispiel "fakultaet1"........................................................................................................20Beispiel "zeichenketten1"................................................................................................26Beispiel "zeichenketten2"................................................................................................27Beispiel "antwort1"..........................................................................................................29Beispiel "antwort2"..........................................................................................................29Beispiel "antwort3"..........................................................................................................31Beispiel "sichtbarkeit"..................................................................................................... 35Beispiel "ausdruck"......................................................................................................... 40Beispiel "incdec"............................................................................................................. 43Beispiel "incdec"............................................................................................................. 43Beispiel "ziffern"............................................................................................................. 46Beispiel "if1"................................................................................................................... 51Beispiel "fliesskomma"................................................................................................... 53Beispiel "void".................................................................................................................55Beispiel "funktionstyp1"..................................................................................................56Beispiel "ref"................................................................................................................... 57Beispiel "ptr"................................................................................................................... 59Beispiel "feld"..................................................................................................................60Beispiel "enum"............................................................................................................... 61Beispiel "funktionstyp2"..................................................................................................68Beispiel "if2"................................................................................................................... 71Beispiel "while"............................................................................................................... 75Beispiel "do"....................................................................................................................76Beispiel "for1"................................................................................................................. 78Beispiel "for2"................................................................................................................. 78Beispiel "aufruf".............................................................................................................. 85Beispiel "param1"............................................................................................................86Beispiel "param2"............................................................................................................87Beispiel "param3"............................................................................................................88Beispiel "fakultaet2"........................................................................................................88Beispiel "oohello"..........................................................................................................105Beispiel "queue1".......................................................................................................... 116Beispiel "const1"........................................................................................................... 123Beispiel "const2"........................................................................................................... 124Beispiel "const3"........................................................................................................... 126Beispiel "ctor"................................................................................................................132Beispiel "file1"...............................................................................................................137Beispiel "file2"...............................................................................................................139Beispiel "copyctor"........................................................................................................144Beispiel "tempobj".........................................................................................................149Beispiel "newdelete"......................................................................................................151Beispiel "stapel".............................................................................................................157

233

Page 242: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger

Beispiel "graphobj1"......................................................................................................163Beispiel "kovarianz"...................................................................................................... 174Beispiel "virtual"........................................................................................................... 179Beispiel "dtor"............................................................................................................... 184Beispiel "multi"............................................................................................................. 186Beispiel "except1"......................................................................................................... 204Beispiel "except2"......................................................................................................... 205Beispiel "rethrow1"....................................................................................................... 213Beispiel "except_raii".................................................................................................... 222

234

Page 243: Objektorientiertes C++ für Einsteigerux-02.ha.bib.de/daten/schulz/projekte/FHDWOS13/Quellen/C++-Skript.pdf · C++ bereits kennen und in einigen kleinen Projekten angewandt haben,

Objektorientiertes C++ für Einsteiger

Literaturverzeichnis[Dijk68] Dijkstra, Edsger W.: Go-to statement considered harmful, 1968, http://ww­

w.cs.utexas.edu/users/EWD/ewd02xx/EWD215.PDF, Abrufzeitpunkt: 03.06.2005

[Ellis90] Ellis, Margaret A.; Stroustrup, Bjarne: The Annotated C++ Reference Ma­nual, , Addison-Wesley, 1990

[GHJV95] Gamma, Erich; Helm, Richard; Johnson, Ralph; Vlissides, John: Design Patterns, Reissue, Addison-Wesley, 1995

[Josu01] Josuttis, Nicolai: Objektorientiertes Programmieren in C++, 2. Auflage, Addison-Wesley, 2001

[Mart96] Martin, Robert C.: The Liskov Substitution Principle, 1996, http://www.ob­jectmentor.com/resources/articles/lsp.pdf, Abrufzeitpunkt: 18.08.2005

[Meye98] Meyers, Scott: Effektiv C++ programmieren, , Addison-Wesley, 1998[Meye99] Meyers, Scott: Mehr Effektiv C++ programmieren, , Addison-Wesley, 1999[Oest01] Oestereich, Bernd: Objektorientierte Softwareentwicklung, 5. Auflage, Ol­

denbourg, 2001[Strou00] Stroustrup, Bjarne: Die C++-Programmiersprache, 4. Auflage, Addison-

Wesley, 2000[Sutt00] Sutter, Herb: Exceptional C++, , Addison-Wesley, 2000[Sutt02] Sutter, Herb: More Exceptional C++, , Addison-Wesley, 2002[Zell03] Zeller, Andreas: Causes and Effects in Computer Programs, 2003,

http://www.st.cs.uni-sb.de/papers/aadebug2003/aadebug.pdf, Abrufzeit­punkt: 16.01.2004

235