175
Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 1 / 175 © Detlef Wilkening 2020 www.wilkening-online.de Objektorientiertes Programmieren in C++ Detlef Wilkening www.wilkening-online.de © 2020 Folien für die C++ Vorlesung FH Aachen WS 2019/20 1 Geschichte ..................................................................................................................... 4

Objektorientiertes Programmieren in C+++Folien.pdf · C++14 war nur ein kleiner, aber wichtiger Schritt. Im Dezember 2017 wurde C++17 publiziert („ISO/IEC 14882:2017“). Der bislang

  • Upload
    others

  • View
    5

  • Download
    0

Embed Size (px)

Citation preview

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 1 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Objektorientiertes Programmieren

in

C++

Detlef Wilkening

www.wilkening-online.de

© 2020

Folien für die C++ Vorlesung

FH Aachen WS 2019/20

1 Geschichte ..................................................................................................................... 4

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 2 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

2 Praxis ............................................................................................................................. 6

2.1 Notepad++ ............................................................................................................... 6

2.2 Microsoft Visual Code .............................................................................................. 6

2.3 vi bzw. vim ................................................................................................................ 6

2.4 Emacs ...................................................................................................................... 6

2.5 IDE’s ........................................................................................................................ 6

2.6 Microsoft Visual Studio Community 2019 ................................................................. 7

2.7 G++ (GCC) ............................................................................................................... 7

2.8 Clang ....................................................................................................................... 7

2.9 Überblick über viele C++ Compiler ........................................................................... 7

2.10 Bibliotheken ............................................................................................................. 7

2.11 Dokumentation ......................................................................................................... 7

3 Einführung ..................................................................................................................... 8

3.1 Die ersten C++ Programme ..................................................................................... 8

3.2 Microsoft Visual Studio ........................................................................................... 11

3.3 Internationalisierung, Lokalisierung & Zeichensätze ............................................... 11

3.4 Debug- und Release-Modus ................................................................................... 13

3.5 Undefined Behaviour .............................................................................................. 13

4 Die grundlegenden Sprach-Elemente ........................................................................ 14

4.1 Elementare Typen .................................................................................................. 14

4.2 Literale ................................................................................................................... 15

4.3 Variablen ................................................................................................................ 16

4.4 Konstanten ............................................................................................................. 20

4.5 Operatoren ............................................................................................................. 22

5 Kontrollstrukturen ....................................................................................................... 29

5.1 Bedingter-Kontrollfluss – „if“ & „else“ ...................................................................... 29

5.2 Mehrfach-Verzweigung – „switch“ .......................................................................... 31

5.3 For-Schleife ............................................................................................................ 34

5.4 While-Schleife ........................................................................................................ 35

5.5 Do-Schleife ............................................................................................................ 35

5.6 Break- und Continue-Anweisungen ........................................................................ 36

5.7 Schleife mit Ausgang in der Mitte ........................................................................... 37

5.8 „goto“ und Labels ................................................................................................... 37

6 Ein- und Ausgabe ........................................................................................................ 38

6.1 Ausgabe ................................................................................................................. 38

6.2 Fehlschläge ............................................................................................................ 40

6.3 Eingabe .................................................................................................................. 42

7 Texte ............................................................................................................................. 45

7.1 „std::string“ ............................................................................................................. 45

7.2 String-Views ........................................................................................................... 51

7.3 String-Wandlungen ................................................................................................ 51

7.4 String-Streams ....................................................................................................... 52

7.5 Reguläre Ausdrücke ............................................................................................... 54

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 3 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

8 STL – Container & Iteratoren ...................................................................................... 56

8.1 Einführung .............................................................................................................. 56

8.2 Iteratoren................................................................................................................ 57

8.3 Vektoren................................................................................................................. 59

8.4 Listen ..................................................................................................................... 63

8.5 Arrays..................................................................................................................... 66

8.6 Sets ........................................................................................................................ 67

8.7 Unordered-Set........................................................................................................ 72

8.8 Maps ...................................................................................................................... 74

8.9 Weiteres zu den STL Containern ........................................................................... 79

9 Typ-System und mehr ................................................................................................. 80

9.1 Typ-Aliase .............................................................................................................. 80

9.2 Referenzen ............................................................................................................ 81

9.3 Aufzählungs-Typen ................................................................................................ 85

9.4 Typ-Konvertierungen .............................................................................................. 87

9.5 Statische Assertions mit „static_assert“ .................................................................. 88

10 Funktionen ............................................................................................................... 89

10.1 Einführung .............................................................................................................. 89

10.2 Parameter und Argumente ..................................................................................... 91

10.3 Rückgaben ............................................................................................................. 94

10.4 Konvertierungen ..................................................................................................... 97

10.5 Default-Argumente ................................................................................................. 99

10.6 Überladen ............................................................................................................ 100

10.7 Parameter und lokale Variablen ........................................................................... 102

10.8 Rekursion ............................................................................................................. 103

10.9 Inline-Funktionen .................................................................................................. 104

10.10 Funktions-Templates ........................................................................................ 105

10.11 Constexpr-Funktionen ...................................................................................... 105

11 Standard-Bibliothek ............................................................................................... 105

11.1 File-Streams ......................................................................................................... 105

11.2 Filesystem-Library ................................................................................................ 109

11.3 Exit ....................................................................................................................... 116

11.4 Zufallszahlen ........................................................................................................ 116

11.5 Mathematische Funktionen .................................................................................. 117

11.6 Pairs und Tuple .................................................................................................... 118

12 Klassen ................................................................................................................... 119

12.1 Motivation ............................................................................................................. 119

12.2 Klassen-Definition ................................................................................................ 120

12.3 Zugriffsbereiche ................................................................................................... 122

12.4 Klassen sind benutzerdefinierte Typen ................................................................. 123

12.5 Erweiterung .......................................................................................................... 123

12.6 Objekt-Zustand .................................................................................................... 124

12.7 Konstruktoren ....................................................................................................... 125

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 4 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

12.8 Destruktoren ........................................................................................................ 137

12.9 Const-Element-Funktionen ................................................................................... 138

12.10 this .................................................................................................................... 140

12.11 Inline ................................................................................................................. 141

12.12 Klassen verwenden Klassen ............................................................................. 141

12.13 Member-Initialisierungs-Listen .......................................................................... 142

12.14 Klassen-Deklarationen ...................................................................................... 144

12.15 Klassen-Elemente ............................................................................................. 145

12.16 friend ................................................................................................................ 147

12.17 Klassenbezogene Typen .................................................................................. 148

13 Operator-Funktionen ............................................................................................. 149

13.1 Einführung ............................................................................................................ 149

13.2 Symmetrische Operatornutzung ........................................................................... 152

13.3 Ausgabe ............................................................................................................... 153

13.4 Kopier-Zuweisungs-Operator = ............................................................................ 154

13.5 Move-Zuweisungs-Operator = .............................................................................. 155

13.6 Funktions-Aufruf Operator .................................................................................... 156

13.7 Spezialitäten ......................................................................................................... 157

13.8 Fazit ..................................................................................................................... 158

14 Vererbung & Polymorphie ..................................................................................... 158

14.1 Vererbung ............................................................................................................ 158

14.2 Konsequenzen aus der „ist-ein“ Beziehung .......................................................... 165

14.3 Polymorphie ......................................................................................................... 167

14.4 Destruktoren ........................................................................................................ 170

14.5 Abstrakte Basis-Klassen ...................................................................................... 171

14.6 Dynamic-Cast ....................................................................................................... 172

14.7 Vererbung & Polymorphie .................................................................................... 174

1 Geschichte

Bjarne Stroustrup startete im Mai 1979 die Entwicklung von C++.

Den Namen C++ gab es damals noch nicht.

Bjarne Stroustrup nannte seine Programmiersprache einfach „C mit Klassen“

1983 wurde „C mit Klassen“ in C++ umbenannt.

1985 schrieb Bjarne Stroustrup das Buch „The C++ Programming Language.

Bis 1990 war dies die inoffizielle Referenz für die Programmiersprache C++.

1985 wurde die ISO Standardisierungsgruppe „ISO/IEC JTC 1/SC 22“ gegründet.

1990 erschien dann das Buch „The Annotated C++ Reference Manual“

Geschrieben von Margaret A. Ellis und Bjarne Stroustrup.

Das Buch wurde die neue inoffizielle Referenz für C++.

1990 wurde das ANSI C++ Komitee gegründet.

1991 wurde die ISO Working Group 21 („ISO/IEC JTC 1/SC 22/WG 21“) gegründet.

Die Working Group 21 ist für C++ zuständig.

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 5 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

1998 erschien der erste C++ Standard: „ISO/IEC 14882:1998“ – kurz C++98 genannt.

C++03 vom November 1993 („ISO/IEC 14882:2003“) war nur eine technische Korrektur.

Im September 2011 wurde der dritte ISO C+ Standard verabschiedet:

„ISO/IEC 14882:2011“ – kurz C++11.

C++11 war ein sehr großer Schritt

Im Dezember 2014 wurde C++14 publiziert („ISO/IEC 14882:2014“).

C++14 war nur ein kleiner, aber wichtiger Schritt.

Im Dezember 2017 wurde C++17 publiziert („ISO/IEC 14882:2017“).

Der bislang letzte und daher aktuelle Standard.

War ein mittlerer Schritt mit vielen Erweiterungen, aber keine großen Features.

2020 wird wohl C++20 erscheinen, mit vielen neuen großen Features.

https://en.wikipedia.org/wiki/C%2B%2B

https://en.wikipedia.org/wiki/ISO/IEC_JTC_1/SC_22

https://en.wikipedia.org/wiki/Bjarne_Stroustrup

https://isocpp.org/std

https://en.cppreference.com/w/cpp/language/history

http://www.trytoprogram.com/cplusplus-programming/history/

https://en.wikipedia.org/wiki/Standard_Template_Library

https://www.iso.org/standard/68564.html

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 6 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

2 Praxis

Um in C++ zu programmieren, benötigen Sie mindestens:

Einen Editor oder eine IDE (integrierte Entwicklungs-Umgebung)

Und einen C++ Compiler

Referenz-Dokumentation

Später sicher noch weitere C++ Bibliotheken

2.1 Notepad++

http://notepad-plus-plus.org/

https://de.wikipedia.org/wiki/Notepad%2B%2B

2.2 Microsoft Visual Code

https://code.visualstudio.com/

https://de.wikipedia.org/wiki/Visual_Studio_Code

2.3 vi bzw. vim

https://de.wikipedia.org/wiki/Vi

https://de.wikipedia.org/wiki/Vim

2.4 Emacs

https://de.wikipedia.org/wiki/Emacs

2.5 IDE’s

https://de.wikipedia.org/wiki/Liste_von_Integrierten_Entwicklungsumgebungen

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 7 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

2.6 Microsoft Visual Studio Community 2019

https://visualstudio.microsoft.com/de/vs/community/

https://de.wikipedia.org/wiki/Visual_C%2B%2B

Achten Sie bei der Installation darauf, dass Sie auch die C++ Unterstützung mit

installieren.

Achtung – Sie müssen sich für das Microsoft Visual Studio bei einem Microsoft Dienst

anmelden.

2.7 G++ (GCC)

https://de.wikipedia.org/wiki/GNU_Compiler_Collection

https://de.wikipedia.org/wiki/MinGW

2.8 Clang

https://de.wikipedia.org/wiki/Clang

https://de.wikipedia.org/wiki/LLVM

2.9 Überblick über viele C++ Compiler

https://de.wikipedia.org/wiki/C%2B%2B#C++-Compiler

https://isocpp.org/get-started

https://en.cppreference.com/w/cpp/compiler_support

2.10 Bibliotheken

http://www.boost.org

https://cpp.zeef.com/faraz.fallahi

https://cpp.libhunt.com/

https://florianjw.de/en/good_libraries.html

https://github.com/fffaraz/awesome-cpp

https://de.wikipedia.org/wiki/Liste_von_GUI-Bibliotheken#C.2B.2B

https://en.wikipedia.org/wiki/List_of_widget_toolkits

https://www.reddit.com/r/cpp/comments/babfl5/a_pretty_big_list_of_c_gui_libraries

2.11 Dokumentation

http://isocpp.org/

http://en.cppreference.com/w/

http://www.cplusplus.com/

http://www.cplusplus.com/reference/

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 8 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

https://stackoverflow.com/

https://stackoverflow.com/questions/tagged/c%2b%2b

3 Einführung

3.1 Die ersten C++ Programme

Das erste C++ Programm

// Das erste C++ Programm

#include <iostream>

int main()

{

std::cout << "Hallo Welt";

}

Allgemein

C++ Quelltext ist formlos

bis auf drei Ausnahmen:

Zeilen-Kommentare

Präprozessor Direktive

Zeichenketten-Konstanten

Kommentare

Von // bis zum Zeilenende ist Kommentar und wird vom Compiler ignoriert.

Dies ist eine der drei Ausnahmen bzgl. formlosen Quelltexts.

#include <iostream>

Präprozessor Direktive

Allgemein:

#include <xyz> bzw. #include “xyz“

macht weitere Deklarationen und Definitionen bekannt

bindet den Header xyz ein

Hier:

macht einen Teil der I/O C++ Standard-Bibliothek bekannt, die damit benutzt werden

kann

bindet den Header „iostream“ ein

Auch Präprozessor Direktiven sind nicht formlos:

sie müssen abgesehen von Kommentaren allein in der Zeile stehen

int main() { ... }

Definiert eine Funktion – hier „main“

Eine Funktion ist ein Programmteil, das einen Namen hat, und von überall her benutzt

werden kann.

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 9 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Für uns hier erstmal wichtig ist, dass es in C++ zwei spezielle Funktionen gibt, die den

Startpunkt des Programms bildet. D.h. genau eine von ihnen muß in einem Programm

vorkommen.

Funktion 1: int main()

Funktion 2: int main(int argc, char* argv[ ])

Weiteren Varianten von „main“, die ihr Compiler möglicherweise unterstützt, sind nicht

Teil von ISO C++. Wie beschränken uns hier auf Variante 1.

Alles weitere zu Funktionen später. Nur noch ein Hinweis für alle, die schon etwas C oder

C++ Kenntnisse haben: Ja, es ist richtig und ernstgemeint, dass die Funktion „main“

keinen „int-Wert“ zurückgibt, obwohl sie mit dem Rückgabe-Typ „int“ definiert ist. Für die

beiden ISO „main“ Funktionen definiert der Standard die Ausnahme, dass das return

optional ist, und wenn nicht vorhanden einem „return 0;“ entspricht.

{ }

Zwei geschweifte Klammern bilden einen Block, der eine Menge von Elementen (auch

eine leere Menge) zu einer Einheit zusammenfasst.

In diesem Fall bildet der Block die Implementierung der Funktionen, d.h. eine Menge von

Deklarationen, Definitionen, und vor allem Anweisungen.

Innerhalb einer Funktion kann ein Block auch stellvertretend für eine Anweisung stehen.

Dies wird z.B. bei den Kontrollstrukturen und Schleifen häufig benötigt und genutzt.

std::cout

„std“ ist der Namensraum für (fast) alle Elemente der C++ Standard-Bibliothek

:: ist der Scope-Resolution-Operator (Bereichs-Zuordnungs-Operator), der dem folgenden

Namen (hier „cout“) einem Bereich zuordnet (hier dem Namensraum „std“).

cout steht für char-output und ist ein Stream-Objekt dass mit der Console für die Ausgabe

verbunden ist. D.h. alle Ausgabe-Elemente die in std::cout hineingeschoben werden,

erzeugen eine Ausgabe auf der Console.

Der Typ von std::cout ist std::ostream.

Für dieses Objekt (und die auf es mögichen Funktionen) mußte der Header iostream

eingebunden werden, da ein C++ Compiler nur Dinge compiliert, die ihm bekannt sind.

Ein Stream ist ein Ein- oder Ausgabestrom von Zeichen, der mit einem Gerät, einer Datei,

o. ä. verbunden ist.

<<

Ausgabe-Operator (in C heißt er Links-Schiebe-Operator)

Mit ihm wird das rechts-stehende Objekt (2ter Operand) in den Stream (1ter Operand)

hineingeschoben.

Viele Objekte (aber bei weitem nicht alle) lassen sich mit dem Ausgabe-Operator in einen

Stream schieben und damit ausgeben. Zumindest für alle Literale und alle elementaren

Datentypen sowie Strings trifft dies zu.

“Hallo Welt“

Zeichenkettenkonstanten stehen in doppelten Anführungszeichen.

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 10 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Zeichenkettenkonstanten dürfen die von C bekannten Backslash-Sequenzen enthalten

wie z.B.: „\n“ für einen Zeilenumbruch, „\t“ für einen Tabulator, „\““, „\\“, usw.

std::cout << “Hallo Welt“;

Bildet eine Anweisung, d.h. etwas was der Computer ausführen soll.

In diesem Fall wird die Zeichenkettenkonstante „Hallo Welt“ in den Ausgabestrom

std::cout geschoben, d.h. auf der Console ausgegeben.

Eine Anweisung wird immer durch ein Semikolon „;“ abgeschlossen.

Anweisungen dürfen nur in Funktionen stehen.

Anweisungen werden nacheinander – in der Reihenfolge ihres Auftauchens im Quelltext –

abgearbeitet. Diese sequentielle Abarbeitung kann durch Schleifen, Kontrollstrukturen,

(implizite und explizite) Funktionsaufrufe und Exceptions beeinflußt werden. Die

Abarbeitung beginnt beim Start des Programms in der Funktion main. Hinweis – dieses

Verfahren nennt man imperativ, weshalb C++ zu den sogenannten imperativen Sprachen

gehört.

Es darf auch leere Anweisungen geben.

Das zweite C++ Programm

#include <iostream>

int main()

{

int var = 3; // (5) Definiert eine Variable 'var'

std::cout << "var: " << var << '\n'; // (6) Ausgabe-Verkettungen und Ausgabe von

'var'

var = var + 2; // (7) Die Variable 'var' wird um 2 erhoeht

std::cout << "var: " << var << '\n'; // (8)

}

Ausgabe

var: 3

var: 5

In Zeile (5) wird eine sogenannte Variable definiert. Der Typ „int“ in der Variablen-

Definition sagt dem Compiler, dass diese Variable Ganzzahlen (sogenannte Integer

Zahlen) aufnehmen können soll. Und wir initialisieren mit „=3“ die Variable direkt mit dem

Wert „3“.

In Zeile (6) führen wir die schon bekannte Ausgabe auf der Konsole mit „std::cout <<“

durch. Aber hier verketten wir mehrere Ausgaben innerhalb der Anweisung. Der Inhalt der

Variablen (hier „3“) wird dann ausgegeben. Am Ende der Anweisung wird dann noch das

Zeichen „\n“ ausgegeben, dass einen Zeilenumbruch darstellt. Die Ausgabe-Zeile in der

Konsole wird also beendet, und die nächste Ausgabe erfolgt in einer neuen Zeile in der

Konsole.

In Zeile (7) addieren wir auf die Variable „var“ den Wert „2“. Diese Anweisung ändert also

den Inhalt von „var“ von „3“ auf „5“.

Zeile (8) entspricht Zeile (6) und gibt den neuen Variablenwert inkl. Text aus.

Nutzung von Namespaces vereinfachen

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 11 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Mit einer Using-Anweisung werden alle Symbole eines Namenraums importiert.

Mit einer Using-Deklaration werden nur die angegebenen Symbole importiert.

In der Praxis sollten Sie vorsichtig mit Using-Anweisungen sein.

#include <iostream>

using namespace std; // Using-Anweisung

int main()

{

cout << "Einfach"; // Benutzung ohne std::

}

#include <iostream>

using std::cout; // Using-Deklaration

int main()

{

cout << "Oder so"; // Benutzung ohne std::

}

3.2 Microsoft Visual Studio

Sie benötigen:

Workspace bzw. Solution bzw. Projektmappe

Projekte

Achtung, es empfiehlt sich:

Anlage eines leeren Workspace

Hinzufügen eines leeren Projektes zum Workspace

Bitte wirklich leeres Projekt nehmen

Ein Microsoft Visual Studio Konsolen-Projekt fügt visual-studio spezifische Elemente

hinzu

Die wollen wir hier nicht, da kein ISO C++

Die Default-Filter (Headerdateien, Quelldateien, Ressourcendateien) können sie löschen

Hinzufügen eines neuen Elementes (C++ Datei) zum Projekt

Erstellen des Programms mit zum Beispiel Menü „Erstellen“ mit „Project-Name erstellen“.

Starten des Programms mit dem Eintrag „Starten ohne Debugging“ im Menü „Debuggen“.

Achtung – bei mehreren Projekten im Workspace müssen sie das Startprojekt definieren

Über das Kontextmenü des Projektes

Das Startprojekt erkennt man an der fetten Schrift

C++17 muss explizit aktiviert werden:

Projekt-Optionen oder Compiler-Flag „/std:c++17“

Alternativ C++20 mit Compiler-Flag „/std:c++latest“

3.3 Internationalisierung, Lokalisierung & Zeichensätze

Diese Themen sprengen den Rahmen der Vorlesung bei weitem.

Sind auch keine spezifischen C++ Themen

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 12 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Trotzdem sollen erwähnt werden:

Zeichensätze bzw. Zeichenkodierungen

Portabilität

Bearbeitung und Vergleich von Zeichen

Sichere Annahmen über Zeichen

Zeichensätze bzw. Zeichenkodierungen

Der Computer speichert intern nur Zahlen ab.

Werden Zeichen benötigt, so kodiert der Computer sie intern in Form von Zahlen.

Die jeweilige Abbildung von einer Zahl auf ein Zeichen wird salopp oft Zeichensatz bzw.

Zeichenkodierung genannt.

Es gibt tausende von Zeichensätzen.

ASCII Zeichensatz

7 bittig => 128 Zeichen (2^7)

Auf diesen beschränken wir uns in der Vorlesung

Er enthält zum Beispiel die deutschen Umlaute nicht

https://de.wikipedia.org/wiki/American_Standard_Code_for_Information_Interchange

Sehr verbreitete 8-Bit Zeichensätze

Zum Beispiel Windows-1252 (auch CP 1252 oder ANSI genannt) und ISO 8859

https://de.wikipedia.org/wiki/Windows-1252

https://de.wikipedia.org/wiki/ISO_8859

Unicode

In den 90-er Jahren wurde ein neuer Zeichensatz entwickelt, der:

von Grund auf alle Probleme von Zeichen adressieren sollte,

und alle Zeichen der Welt enthalten sollte.

https://de.wikipedia.org/wiki/Unicode

Diesen sollten Sie in der Praxis bei echten Programmen nutzen

Portabilität

Jedes Betriebssystem verwendet einen anderen 8-Bit Zeichensatz.

Aber sie sind fast alle auf ASCII basiert

Und sie unterstützen Unicode

Vorsicht beim

Übertragen von Quelltexten zwischen verschiedenen Systemen

Oder bei der Cross-Compilierung

=> Beispiel Microsoft Visual Studio und Konsole mit Umlauten

Bearbeitung und Vergleich von Zeichen

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 13 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Typische Probleme:

Groß- und Kleinschreibung

Sortieren ist nicht lexikalisch

Verwenden Sie in realen Programmen entsprechende Bibliotheken, zum Beispiel:

http://site.icu-project.org/design/cpp

https://docs.microsoft.com/en-us/windows/win32/intl/international-components-for-

unicode--icu-

https://www.boost.org/doc/libs/1_71_0/libs/locale/doc/html/building_boost_locale.html

https://github.com/tzlaine/text

Darum beschränken sich die Beispiele oft auf Kleinbuchstaben

Sichere Annahme über Zeichen

Im Prinzip können Sie sich bei Zeichen auf fast nichts verlassen.

Der Standard legt fest:

Das Zeichen ‚\n‘ ist immer ein Zeilenumbruch, und wird plattform-spezifisch

interpretiert.

Die Zeichen ‘0‘..‘9‘ liegen im Zeichensatz bündig hintereinander

3.4 Debug- und Release-Modus

In C++ unterscheidet man zwischen verschiedenen Modi.

Typisch sind Debug- und der Release-Modus

Im Microsoft Visual Studio Debug- und Release-Konfiguration genannt

Der Debug-Modus ist für die Entwicklung gedacht

Der Release-Modus ist für das endgültige Artefakt gedacht

Machen Sie Performance Messungen nur im Release-Modus

Daher mindestens mit aktivierter Optimierung

3.5 Undefined Behaviour

Nicht korrekte C++ Programme erzeugen häufig UB (Undefined Behaviour)

Wegen der Fokussierung auf Performance

Keine überflüssigen Abfragen

Im UB Fall kann wirklich alles passieren

Im Debug-Modus liefern IDE und Compiler häufig Hilfen

Aber das ist nicht immer der Fall

Beispiel:

// Achtung, dieses Programm ist fehlerhaft

#include <iostream>

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 14 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

#include <string>

using namespace std;

int main()

{

string s("Hallo");

cout << s[1]; // Dieser Zugriff ist okay

cout << s[9]; // Dieser Zugriff ist fehlerhaft => Undefined behaviour

}

4 Die grundlegenden Sprach-Elemente

4.1 Elementare Typen

Es gibt vier Arten von elementaren Typen:

Wahrheitswerte

Ganz-Zahlen

Fließkomma-Zahlen

Zeichen

Wahrheitswerte

bool b = true;

Integer-Zahlen

Der Unterschied zwischen den verschiedenen Integer-Typen ist ihre Größe.

Uns stehen in C++ folgende vorzeichenbehaftete Integer-Typen zur Verfügung (der Größe

nach sortiert):

Außerdem gibt es noch die entsprechenden vorzeichenlosen Integer-Typen.

signed char

short

int

long

long long

1 Byte == sizeof(signed char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)

Häufig finden Sie für 64 Bit Betriebssystem folgende Größen:

signed char 8 Bit 8 Bit

short 16 Bit 16 Bit

int 32 Bit 32 Bit

long 32 Bit 64 Bit

long long 64 Bit 64 Bit

https://de.wikipedia.org/wiki/64-Bit-Architektur#Programmiermodell

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 15 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

https://en.cppreference.com/w/cpp/language/types

http://www.cplusplus.com/reference/cstdint/

Welchen Zahlenumfang decken die Integer-Typen ab?

8 Bit -128 … +127

16 Bit -32.768 … +32.768

32 Bit -2.147.483.648 … +2.147.483.647

64 Bit -9.223.372.036.854.775.808 … +9.223.372.036.854.775.807

int x = 3;

Fließkomma-Zahlen

Fließkomma-Typen:

float

double

long double

sizeof(float) <= sizeof(double) <= sizeof(long double)

Achtung – viele Probleme bei Fließkomma-Typen, siehe zum Beispiel:

https://www.itu.dk/~sestoft/bachelor/IEEE754_article.pdf

double d = 3.14;

Zeichen

C++ fährt bei Zeichen und Texten dreigleisig.

1 Byte Zeichensatz, meist ASCII basiert

Typ char

Nur das machen wir in der Vorlesung

Wide-Character

Typ wchar_t

Unicode

Typen „char8_t“ (ab C++20), „char16_t“ und „char32_t“

4.2 Literale

Literale sind Werte im Quelltext

bool b1 = true;

bool b2 = false;

int x1 = 0;

int x2 = 123;

int x3 = -897;

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 16 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

long x4 = 12L; // Postfix "L" => Typ "long"

long long x5 = 42LL; // Postfix "LL" => Typ "long long"

int x1 = 0b1011; // Binaer-Zahl => Wert ist 11

int x2 = 012; // Oktal-Zahl => Wert ist 10

int x3 = 0x1A; // Hexadezimal-Zahl => Wert ist 26

int x1 = 123'456;

long long x2 = 123'456'789'012'345LL;

double d1 = 1.;

double d2 = .4;

double d3 = 31.459;

float f = 2.7F;

double d = 4.5E+12; // => 4,5 * 10^12

char c = 'A';

char c1 = '\''; // Das einfache Hochkommata '

char c2 = '\\'; // Der Backslash \ selber

char c3 = '\n'; // Ein Zeilenumbruch

Eine Liste aller Escape-Sequenzen:

https://en.cppreference.com/w/cpp/language/escape

Zeichenketten-Konstanten sind keine Strings

string s1("");

string s2("C++");

string s3("Hallo Welt");

string s1("Text mit \"\\\""); // => Text mit "\"

string s2("Zeile 1\nZeile2"); // 2 Zeilen dank Zeilenumbruch \n

Postfix „s“ macht aus einer Zeichenketten-Konstante einen String-Literal mit Typ „std::string“

#include <string>

using namespace std; // Using-Anweisung fuer Postfix s

string s = "C++"s; // String-Literal dank Postfix s

#include <string>

using std::string;

using std::operator""s; // Using-Deklaration fuer Postfix s

string s = "C++"s; // String-Literal dank Postfix s

4.3 Variablen

Variablen-Arten

Lokale Variablen sind Variablen, die in einer Funktion definiert werden.

Sie sind lokal zur Funktion, sind nur dort bekannt, und können nur dort benutzt.

Globale Variablen sind alle Variablen, die außerhalb von Funktionen und Klassen definiert

werden.

Sie existieren die gesamte Laufzeit des Programms und können prinzipiell von überall

aus genutzt werden.

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 17 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

int g = 11; // Globale Variable

int main()

{

int x = 22; // Lokale Variable

}

Lokale Variablen:

int main() // "main" als Beispiel fuer eine Funktion

{

int n1; // Hier wird n1 erzeugt => n1 ist nun sichtbar und kann genutzt werden

n1 = 2; // Okay, denn n1 existiert und ist sichtbar

n2 = 3; // Compiler-Fehler – n2 existiert nicht

int n2; // Hier wird n2 erzeugt => n2 ist nun sichtbar und kann genutzt werden

n2 = 4; // Okay, denn nun existiert n2

{ // Neuer Scope

int n3; // Hier wird n3 erzeugt

n3 = 5; // Okay, denn n3 exisitiert und ist sichtbar

n1 = 6; // Okay, n1 ist auch hier sichtbar und existent

} // Hier wird n3 zerstoert

n3 = 8; // Compiler-Fehler – n3 ist zerstoert und auch nicht mehr sichtbar

n2 = 9; // Hier wird wieder n2 angesprochen

n1 = 10; // Okay, n1 ist hier natuerlich bekannt

} // Hier werden n2 und n1 zerstoert

Globale Variablen:

#include <iostream>

using namespace std;

int g = 42; // Globale Variable

void fct()

{

cout << "fct - g: " << g << '\n';

g = 32;

}

int main()

{

cout << "g: " << g << '\n';

++g;

cout << "g: " << g << '\n';

fct();

cout << "g: " << g << '\n';

}

Ausgabe

g: 42

g: 43

fct - g: 43

g: 32

Variablen Definitionen

Default-Initialization

Copy-Initialization

Direct-Initialization

Direct-List-Initialization

Auto & Decltype Initialization

Multiple-Definitions

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 18 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Structured Binding Declaration

Default-Initialization

int main()

{

int x; // Undefinierter Initial-Wert

cout << x; // Gibt eine rein zufaellige Zahl aus

string s; // Definierter Initial-Wert: Leerstring

cout << s; // Gibt immer einen Leerstring aus

}

Copy-Initialization

int var = 42; // Copy-Initialization

string s = "C++"; // Kein guter Code, denn der Typ rechts ist nicht gleich dem Typ links

Direct-Initialization

double d(3.14);

char c('A');

string name("Tom"); // String mit Inhalt "Tom"

string s(5, '*'); // String mit Inhalt "*****"

Direct-List-Initialization

int n1{}; // Definiert "n1" mit dem Initial-Wert von "0"

int n2{ 2 };

double d{3.14};

char c{'A'};

string s1{}; // Leerstring

string s2{ "C++" };

Aber Achtung, manchmal macht Direct-List-Initialization was anderes.

#include <iostream>

#include <string>

using namespace std;

int main()

{

string s{65, 'B'}; // Erzeugt NICHT einen String mit 65 B's

// Sondern meist einen String mit Inhalt "AB"

cout << s << '\n';

}

Mögliche Ausgabe (nur auf Plattformen mit ASCII-basiertem Zeichensatz)

AB

Auto & Decltype Initialization

Mit „auto“ wird der Typ anhand des Typs des Initial-Wertes bestimmt.

Mit „decltype“ wird der Typ anhand eines Beispiel-Ausdrucks bestimmt.

Mit „decltype(auto)“ wird der Typ wie bei „auto“ anhand des Typs des Initial-Wertes

bestimmt, aber mit den decltype Regeln.

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 19 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Automatische Typ-Deduktion mit "auto"

#include <iostream>

using namespace std;

int main()

{

// Literal "42" hat den Typ "int" -> Variable "n1" hat den Typ "int"

auto n1 = 42; // Zuweisungs-Operator

cout << n1 << '\n';

// Literal "44" hat den Typ "int" -> Variable "n2" hat den Typ "int"

auto n2(44); // Runde Klammern

cout << n2 << '\n';

// Variable "n1" hat den Typ "int" -> Variable "n3" hat den Typ "int"

auto n3{n1}; // Geschweifte Klammern

cout << n3 << '\n';

// Var. "n2" hat den Typ "int", "2*n2" dann auch -> Variable "n4" hat den Typ "int"

auto n4 = 2*n2;

cout << n4 << '\n';

// Literal "3.14" hat den Typ "double" -> Variable "d" hat den Typ "double"

auto d{3.14};

cout << d << '\n';

}

Ausgabe

42

44

42

88

3.14

Gerade bei komplexen Typen ist „auto“ oft sehr hilfreich.

#include <vector>

using namespace std;

int main()

{

vector<int> v;

vector<int>::iterator iter1 = begin(v); // Viel "sinnlose" Schreibarbeit

auto iter2 = begin(v); // Viel kuerzer und einfacher

}

Achtung, Zeichenketten-Literale sind nicht vom Typ „std::string“!

auto x = "C++"; // "x" ist KEIN String

Automatische Typ-Deduktion mit "decltype"

std::string fct(); // Deklaration einer Funktion "fct", die einen

// "string" zurueckgibt – siehe spaeteres Kapitel

int n = 6;

decltype(n) x1 = n; // "n" ist ein "int" => "x1" ist ein "int"

decltype(1+2) x2; // "1+2" ist ein "int" => "x2" ist ein "int"

decltype(x1<x2) x3; // "x1<x2" ist ein "bool" => "x3" ist ein "bool"

decltype(fct()) x4; // "fct()" gibt einen String zurueck => "x4" ist ein "std::string"

Automatische Typ-Deduktion mit "decltype(auto)"

int x = 1;

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 20 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

decltype(auto) value = x; // Typ-Deduktion vom Initial-Wert mit decltype Regeln

Multiple-Definitions

Es ist in C++ möglich, mehrere Variablen auf einmal zu definieren.

#include <iostream>

#include <string>

using namespace std;

int main()

{

int x1 = 1, x2(2), x3{3}; // Multiple Definition von Int-Variablen

cout << "x1: " << x1 << '\n';

cout << "x2: " << x2 << '\n';

cout << "x3: " << x3 << '\n';

string s1, s2("abc"), s3{"XYZ"}; // Multiple Definition von String-Variablen

cout << "s1: " << s1 << '\n';

cout << "s2: " << s2 << '\n';

cout << "s3: " << s3 << '\n';

auto a1 = 11, a2(22), a3{33}; // Multiple Definition von Int-Variablen

cout << "a1: " << a1 << '\n';

cout << "a2: " << a2 << '\n';

cout << "a3: " << a3 << '\n';

}

Ausgabe

x1: 1

x2: 2

x3: 3

s1:

s2: abc

s3: XYZ

a1: 11

a2: 22

a3: 33

Structured Binding Declaration

#include <iostream>

#include <tuple>

using namespace std;

int main()

{

tuple t(42, 3.1415, 'x');

auto [a,b,c] = t; // Structured Binding Declaration

cout << "a: " << a << '\n';

cout << "b: " << b << '\n';

cout << "c: " << c << '\n';

}

Ausgabe

a: 42

b: 3.1415

c: x

4.4 Konstanten

Konstanten drücken aus, dass man Variablen-Werte nicht ändern möchte.

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 21 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Diese Semantik kann der Compiler nun überprüfen.

Code mit Konstanten ist oft besser lesbar und leichter änderbar.

Konstanten werden häufig eingesetzt, um Literale im Code zu ersetzen

Der Compiler kann bei Konstanten oft performanteren Code erzeugen.

„constexpr“ vor einer Variablen macht sie zu einer Compile-Zeit Konstanten

constexpr double mwst_mult = (1.0 + mwst/100.0);

...

double betrag = sum * mwst_mult;

Benötigen Sie eine Laufzeit-Konstante, dann nutzen Sie „const“.

const int konstante = lese_wert_von_konsole_ein();

Der Compiler schützt die Konstanten:

constexpr int ci1 = 11;

ci1 = 33; // Compiler-Fehler – ci1 ist Konstante

const int ci2 = 22;

ci2 = 33; // Compiler-Fehler – ci2 ist Konstante

Konstanten müssen natürlich initialisiert werden.

constexpr int g; // Compiler-Fehler – Konstanten muessen initialisiert werden

const int v; // Compiler-Fehler – Konstanten muessen initialisiert werden

Kombination von „const“, „auto“ und „decltype“

#include <iostream>

using namespace std;

int main()

{

const int n = 11;

auto a = n; // "a" ist nur ein "int" – ohne "const"

a = 20; // Okay, denn "a" ist nur eine normale Variable

} // "auto" ignoriert "const"

#include <iostream>

using namespace std;

int main()

{

const int n = 11;

decltype(auto) a = n; // "a" ist ein "const int"

a = 20; // Compiler-Fehler - "a" ist eine Konstante

} // "decltype" ignoriert "const" NICHT

„const“ kann mit der automatische Typ-Deduktion mit „auto“ manuell kombiniert werden.

#include <iostream>

using namespace std;

int main()

{

int n = 11;

const auto a = n;

a = 20; // Compiler-Fehler, da "a" eine Konstante ist

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 22 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

}

Const ist linksbindend, außer...

int const gap = 4;

Steht links vom const kein Typ, so bindet „const“ nach rechts.

const int gap = 4; // Auch okay

Beide Varianten sind absolut gleichwertig.

4.5 Operatoren

Eine Auflistung aller Operatoren am Ende des Kapitels, inkl. Priorität und Asssoziativität.

Zuweisung

#include <iostream>

using namespace std;

int main()

{

int n;

int m = 4; // Achtung – keine Zuweisung, sondern Initialisierung

n = m+2; // Zuweisung – der Wert von "m+2" wird "n" zugewiesen

cout << "n: " << n;

}

Ausgabe

n: 6

Operative Zuweisung

#include <iostream>

using namespace std;

int main()

{

int n;

int m = 4;

n *= 3+m; // entspricht: n = n * (3+m); => 35

cout << "n: " << n << '\n';

n -= m + 2; // entspricht: n = n - (m+2); => 29

cout << "n: " << n << '\n';

}

Ausgabe

n: 35

n: 29

#include <iostream>

using namespace std;

int main()

{

int n, m;

int o = 4;

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 23 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

n = m = o;

cout << "n: " << n << '\n';

cout << "m: " << m << '\n';

cout << "o: " << o << '\n';

n = 3 + (m = 4 * (o += 2));

cout << "n: " << n << '\n';

cout << "m: " << m << '\n';

cout << "o: " << o << '\n';

}

Ausgabe

n: 4

m: 4

o: 4

n: 27

m: 24

o: 6

Mathematische Operatoren

#include <iostream>

using namespace std;

int main()

{

// Integer-Zahlen

cout << "2+3 => " << 2+3 << '\n'; // => 5

cout << "8-6 => " << 8-6 << '\n'; // => 2

cout << "3*4 => " << 3*4 << '\n'; // => 12

// Fliesskomma-Zahlen

cout << "2.3+3.2 => " << 2.3+3.2 << '\n'; // => 5.5

cout << "8.8-6.6 => " << 8.8-6.6 << '\n'; // => 2.2

cout << "3.5*4.0 => " << 3.5*4.0 << '\n'; // => 14

}

Ausgabe

2+3 => 5

8-6 => 2

3*4 => 12

2.3+3.2 => 5.5

8.8-6.6 => 2.2

3.5*4.0 => 14

Geteilt-Operator

#include <iostream>

using namespace std;

int main()

{

cout << "8.8/4.4 => " << 8.8/4.4 << '\n'; // => 2

cout << "9.0/2.0 => " << 9.0/2.0 << '\n'; // => 4.5

}

Ausgabe

8.8/4.4 => 2

9.0/2.0 => 4.5

#include <iostream>

using namespace std;

int main()

{

cout << "7/1 => " << 7/1 << '\n'; // => 7

cout << "7/2 => " << 7/2 << '\n'; // => 3

cout << "7/3 => " << 7/3 << '\n'; // => 2

cout << "7/4 => " << 7/4 << '\n'; // => 1

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 24 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

cout << "7/5 => " << 7/5 << '\n'; // => 1

cout << "7/6 => " << 7/6 << '\n'; // => 1

cout << "7/7 => " << 7/7 << '\n'; // => 1

cout << "7/8 => " << 7/8 << '\n'; // => 0

cout << "7/9 => " << 7/9 << '\n'; // => 0

}

Ausgabe

7/1 => 7

7/2 => 3

7/3 => 2

7/4 => 1

7/5 => 1

7/6 => 1

7/7 => 1

7/8 => 0

7/9 => 0

Fließkomma-Typ Ergebnis

#include <iostream>

using namespace std;

int main()

{

double d = 2.;

cout << 5./2 << '\n'; // Fliesskomma-Literal

cout << 5/2. << '\n'; // Fliesskomma-Literal

cout << 7/d << '\n'; // Fliesskomma-Variable

cout << 9/static_cast<double>(2) << '\n'; // Explizite Typ-Konvertierung

}

Ausgabe

2.5

2.5

3.5

5.5

Modulo-Operator

#include <iostream>

using namespace std;

int main()

{

cout << "7/1 => " << 7/1 << " Rest " << 7%1 << '\n'; // => 7 Rest 0

cout << "7/2 => " << 7/2 << " Rest " << 7%2 << '\n'; // => 3 Rest 1

cout << "7/3 => " << 7/3 << " Rest " << 7%3 << '\n'; // => 2 Rest 1

cout << "7/4 => " << 7/4 << " Rest " << 7%4 << '\n'; // => 1 Rest 3

cout << "7/5 => " << 7/5 << " Rest " << 7%5 << '\n'; // => 1 Rest 2

cout << "7/6 => " << 7/6 << " Rest " << 7%6 << '\n'; // => 1 Rest 1

cout << "7/7 => " << 7/7 << " Rest " << 7%7 << '\n'; // => 1 Rest 0

cout << "7/8 => " << 7/8 << " Rest " << 7%8 << '\n'; // => 0 Rest 7

cout << "7/9 => " << 7/9 << " Rest " << 7%9 << '\n'; // => 0 Rest 7

}

Ausgabe

7/1 => 7 Rest 0

7/2 => 3 Rest 1

7/3 => 2 Rest 1

7/4 => 1 Rest 3

7/5 => 1 Rest 2

7/6 => 1 Rest 1

7/7 => 1 Rest 0

7/8 => 0 Rest 7

7/9 => 0 Rest 7

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 25 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Vergleichs Operatoren

#include <iostream>

using namespace std;

int main()

{

constexpr int v1 = 1;

constexpr int v2 = 2;

constexpr bool v1_equal_v2 = v1 == v2;

constexpr bool v1_unequal_v2 = v1 != v2;

constexpr bool v1_less_v2 = v1 < v2;

constexpr bool v1_less_equal_v2 = v1 <= v2;

constexpr bool v1_greater_v2 = v1 > v2;

constexpr bool v1_greater_equal_v2 = v1 >= v2;

cout << boolalpha; // Bitte noch ignorieren (*)

cout << "v1 == v2 => " << v1_equal_v2 << '\n';

cout << "v1 != v2 => " << v1_unequal_v2 << '\n';

cout << "v1 < v2 => " << v1_less_v2 << '\n';

cout << "v1 <= v2 => " << v1_less_equal_v2 << '\n';

cout << "v1 > v2 => " << v1_greater_v2 << '\n';

cout << "v1 >= v2 => " << v1_greater_equal_v2 << '\n';

}

Ausgabe

v1 == v2 => false

v1 != v2 => true

v1 < v2 => true

v1 <= v2 => true

v1 > v2 => false

v1 >= v2 => false

Logische „Und“ und „Oder“ Operatoren

Op. 1 Op. 2 && (Und) Op. 1 Op. 2 || (Oder)

false false false false false false

true false false true false true

false true false false true true

true true true true true true

#include <iostream>

using namespace std;

int main()

{

cout << boolalpha; // Bitte noch ignorieren (*)

cout << "false && false => " << (false && false) << '\n'; // => false

cout << "false && true => " << (false && true) << '\n'; // => false

cout << "true && false => " << (true && false) << '\n'; // => false

cout << "true && true => " << (true && true) << '\n'; // => true

bool b1 = false;

bool b2 = true;

bool or1 = b1 || b2;

bool or2 = b1 || false;

cout << "or1: " << or1 << '\n'; // => true

cout << "or2: " << or2 << '\n'; // => false

}

Ausgabe

false && false => false

false && true => false

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 26 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

true && false => false

true && true => true

or1: true

or2: false

Kurzschluss-Auswertung

#include <iostream>

using namespace std;

bool f_true()

{

cout << "- f_true()\n";

return true;

}

bool f_false()

{

cout << "- f_false()\n";

return false;

}

int main()

{

cout << boolalpha;

cout << "f_true() && f_false()\n";

bool b1 = f_true() && f_false(); // Ruft beide Funktionen auf

cout << "> " << b1 << '\n';

cout << '\n';

cout << "f_false() && f_true()\n";

bool b2 = f_false() && f_true(); // Ruft nur "f_false" auf

cout << " => " << b2 << '\n';

}

Ausgabe

f_true() && f_false()

- f_true()

- f_false()

> false

f_false() && f_true()

- f_false()

=> false

Prä- und Post-Inkrement und -Dekrement Operatoren

#include <iostream>

using namespace std;

int main()

{

int n = 4;

cout << n << " ++ => ";

++n; // Prae-Increment Operator

cout << n << " ++ => ";

n++; // Post-Increment Operator

cout << n << '\n';

n = 4;

cout << n << " -- => ";

--n; // Prae-Decrement Operator

cout << n << " -- => ";

n--; // Post-Decrement Operator

cout << n << '\n';

}

Ausgabe

4 ++ => 5 ++ => 6

4 -- => 3 -- => 2

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 27 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Prä- und Postfix-Notation verhalten sich unterschiedlich:

Präfix-Notation „++x“ bzw. „--x"

Zuerst wird die Variable verändert und dann als Ausdrucks-Ergebnis ausgewertet

1. Verändern von „x“

2. Auswerten von „x“

Postfix-Notation „x++“ bzw. „x--"

Zuerst wird die Variable als Ausdrucks-Ergebnis ausgewertet und dann verändert

1. Auswerten von „x“

2. Verändern von „x“

#include <iostream>

using namespace std;

int main()

{

int n = 4;

cout << "n: " << n << '\n';

cout << "++n => " << ++n << '\n'; // Liefert schon den neuen Wert "5"

cout << "n: " << n << '\n';

cout << "n++ => " << n++ << '\n'; // liefert noch den alten Wert "5"

cout << "n: " << n << '\n'; // Hier ist "n" dann auch "6"

}

Ausgabe

n: 4

++n => 5

n: 5

n++ => 5

n: 6

Fragezeichen-Operator

#include <iostream>

using namespace std;

int main()

{

cout << boolalpha;

bool flag = true;

int n = flag ? 3 : 4;

cout << "flag: " << flag << " => n: " << n << '\n';

flag = false;

n = flag ? 3 : 4;

cout << "flag: " << flag << " => n: " << n << '\n';

int m = n*(flag ? 1 : -1) + 5;

cout << "flag: " << flag << " => m: " << m << '\n';

}

Ausgabe

flag: true => n: 3

flag: false => n: 4

flag: false => m: 1

Operatoren-Liste

https://en.cppreference.com/w/cpp/language/operator_precedence

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 28 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Vorrang Operator Bedeutung Assoziativität

1. :: Bereichszuordnung ---

2. -> Elementzugriff L -> R

[ ] Array-Zugriff (Index-Operator) L -> R

( ) Funktionsaufruf L -> R

++ -- Post-Increment bzw. –Decrement L -> R

const_cast Konvertierungs-Operatoren L -> R

static_cast

reinterpret_cast

dynamic_cast

3. sizeof Größe R -> L

++ -- Prä-Increment bzw. –Decrement R -> L

~ 1er Komplement R -> L

! Logisches not R -> L

+ - Vorzeichen (unäres + bzw. -) R -> L

& Adresse R -> L

* Dereferenzierung R -> L

new delete Dynamische Speicherverwaltung R -> L

( ) Klassischer C Cast R -> L

5. * Multiplikation L -> R

/ Division L -> R

% Modulo L -> R

6. + Addition L -> R

- Subtraktion L -> R

7. << >> Bit-Links- bzw. Bit-Rechts-Schieben

oder auch Ein- bzw. Ausgabe-

Operator

L -> R

8. < Kleiner L -> R

> Größer L -> R

<= Kleiner gleich L -> R

>= Größer gleich L -> R

9. == Gleich L -> R

!= Ungleich L -> R

10. & Bitweises Und L -> R

11. ^ Bitweises XOR L -> R

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 29 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

12. | Bitweises Oder L -> R

13. && Logisches Und L -> R

14. || Logisches Oder L -> R

15. ? : Bedingung R -> L

16. = Zuweisung R -> L

*= Operative Zuweisungen

/=

%=

+=

-=

<<=

>>=

&=

|=

^=

5 Kontrollstrukturen

5.1 Bedingter-Kontrollfluss – „if“ & „else“

Syntax:

if (ausdruck)

anweisung

#include <iostream>

using namespace std;

int main()

{

int n = 5;

if (n < 7) // Dieser Ausdruck ist true, da 5 kleiner als 7 ist

cout << "n<7\n"; // => Darum wird diese Ausgabe ausgefuehrt

if (n > 20) // Dieser Ausdruck ist false, da 5 NICHT grosser als 20 ist

cout << "n>20\n"; // => Darum wird diese Ausgabe NICHT ausgefuehrt

if (n != 6) // Dieser Ausdruck ist true, da 5 ungleich 6 ist

cout << "n!=6\n"; // => Darum wird diese Ausgabe ausgefuehrt

}

Ausgabe

n<7

n!=6

#include <iostream>

using namespace std;

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 30 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

int main()

{

int n = 5;

if (n<7)

{

cout << "n<7\n";

cout << "n ist " << n << '\n';

cout << "Und weiter geht es...\n";

}

}

Ausgabe

n<7

n ist 5

Und weiter geht es...

Syntax:

if (ausdruck)

anweisung

else

anweisung

Beispiel:

#include <iostream>

using namespace std;

int main()

{

int n = 5;

if (n<7)

cout << "n<7\n";

else

cout << "n>=7\n";

if (n>20)

cout << "n>20\n";

else

cout << "n<=20\n";

}

Ausgabe

n<7

n<=20

#include <iostream>

using namespace std;

int main()

{

int n = 5;

if (n<7)

{

cout << "True-Zweig\n";

cout << "n<7\n";

{

else

{

cout << "False-Zweig\n";

cout << "n>=7ßn";

}

}

Ausgabe

True-Zweig

n<7

Syntax:

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 31 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

if (ausdruck1)

anweisung1;

else if (ausdruck2)

anweisung;

else

anweisung;

Beispiel:

#include <iostream>

using namespace std;

int main()

{

int age = 27;

if (age<10)

cout << "Kind im Alter von " << age << '\n';

else if (age<18)

cout << "Jugendlicher im Alter von " << age << '\n';

else if (age<30)

cout << "Junger Erwachsener im Alter von " << age << '\n';

else

cout << "Erwachsener im Alter von " << age << '\n';

}

Ausgabe

Junger Erwachsener im Alter von 27

Syntax:

if (Initialisierungs-Teil; ausdruck)

anweisung

#include <iostream>

using namespace std;

int main()

{

const int i = 7;

if (int v = i*i; v > 42) // Variable v ist in der

If-Anweisung bekannt

cout << "i^2 = " << v << " und ist groesser als 42\n";

else

cout << "i^2 = " << v << " und ist kleiner-gleich 42\n";

}

Ausgabe

i^2 = 49 und ist groesser als 42

if (int v = i*i; v > 42) // Variable v ist in der If-

Anweisung bekannt

cout << "i^2 = " << v << " und ist groesser als 42\n";

else

cout << "i^2 = " << v << " und ist kleiner-gleich 42\n";

cout << v; // Compiler-Fehler, v ist

nicht mehr bekannt

5.2 Mehrfach-Verzweigung – „switch“

Syntax

switch (ausdruck) // <- Hier gibt es noch eine zweite Variante mit

Initialisierungs-Teil, s.u.

{

case constant1: // <- diese Konstante muss compile-zeit-konstant sein

anweisung;

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 32 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

...

break; // <- break ist optional

case constant2: // <- diese Konstante muss compile-zeit-konstant sein

anweisung;

...

break; // <- break ist optional

...

default: // <- der Default-Block ist optional – muss also nicht

vorkommen

anweisung;

...

break; // <- break ist optional

}

Beispiel:

#include <iostream>

using namespace std;

int main()

{

const int i = 2;

switch (i)

{

case 1:

cout << "i ist 1\n";

break;

case 2: // Da i==2 ist, wird dieser Block ausgefuehrt

cout << "i ist 2\n";

break; // Dieses break beendet die Switch-Anweisung

case 3:

cout << "i ist 3\n";

break;

}

}

Ausgabe

i ist 2

Beispiel:

#include <iostream>

using namespace std;

int main()

{

const int i = 2;

switch (i)

{

case 1:

cout << "i ist 1\n";

break;

case 2: // Da i==2 ist, wird dieser Block ausgefuehrt

cout << "i ist 2\n";

case 3:

cout << "i ist 3, oder auch nicht...\n"; // Ohne break landen wir auch hier

break;

}

}

Ausgabe

i ist 2

i ist 3, oder auch nicht

Beispiel:

#include <iostream>

using namespace std;

int main()

{

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 33 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

const int i = 2;

switch (i)

{

case 1:

cout << "i ist 1\n";

break;

case 3:

cout << "i ist 3\n";

break;

default: // Ohne passenden Case-Block wird hierhin

verzweigt

cout << "Welchen Wert hat i?\n";

break;

}

}

Ausgabe

Welchen Wert hat i?

Beispiel:

#include <iostream>

using namespace std;

int main()

{

for (int i=0; i<7; ++i)

{

switch (i)

{

case 1:

cout << "i ist 1\n";

break;

case 2:

case 3:

cout << "i ist 2 oder 3\n";

case 4:

cout << "Fluss kommt aus 2/3 oder i ist 4\n";

break;

default:

cout << "i ist weder 1,2,3 oder 4\n";

break;

}

}

}

Ausgabe

i ist weder 1,2,3 oder 4

i ist 1

i ist 2 oder 3

Fluss kommt aus 2/3 oder i ist 4

i ist 2 oder 3

Fluss kommt aus 2/3 oder i ist 4

Fluss kommt aus 2/3 oder i ist 4

i ist weder 1,2,3 oder 4

i ist weder 1,2,3 oder 4

Switch-Anweisung mit Initialisierung-Teil

#include <iostream>

using namespace std;

int main()

{

const int i = 3;

switch (int v = i * i; v)

{

case 4:

cout << "Das Quadrat ist " << v << '\n';

break;

default:

cout << "Das Quadrat ist " << v << " und ungleich 4\n";

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 34 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

}

}

Ausgabe

Das Quadrat ist 9 und ungleich 4

Break, Durchfallen und Fallthrough

#include <iostream>

using namespace std;

int main()

{

for (int i = 0; i < 3; ++i)

{

cout << "i==" << i << " => ";

switch (i)

{

case 0:

case 1:

cout << "0,1 ";

[[fallthrough]]; // Attribut fallthrough, um Warnung zu

unterdruecken

case 2:

cout << "0,1,2 ";

break;

}

cout << '\n';

}

}

Ausgabe

i==0 => 0,1 0,1,2

i==1 => 0,1 0,1,2

i==2 => 0,1,2

5.3 For-Schleife

Syntax

for (init; test; update)

anweisung

Zählschleife

#include <iostream>

using namespace std;

int main()

{

for (int i=0; i<3; ++i)

{

cout << i << ' ';

}

cout << "Fertig\n";

}

Ausgabe

0 1 2 Fertig

for (int i=0; i<3; ++i)

{

cout << i << ' ';

}

cout << i << '\n'; // Compiler-Fehler – die Variable 'i' ist hier nicht mehr bekannt

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 35 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Range-Basierte For-Schleife

#include <iostream>

#include <vector>

using namespace std;

int main()

{

vector v{ 2, 4, 8, 1 };

for (int value : v)

{

cout << value << ' ';

}

cout << '\n';

}

Ausgabe

2 4 8 1

5.4 While-Schleife

Syntax:

while (ausdruck)

anweisung

Beispiel:

#include <iostream>

using namespace std;

int main()

{

int i = 0;

while (i<3)

{

cout << i << ' ';

++i;

}

}

Ausgabe

0 1 2

5.5 Do-Schleife

Syntax:

do

anweisung

while (ausdruck);

Beispiel:

#include <iostream>

using namespace std;

int main()

{

int i = 0;

do

{

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 36 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

cout << i << ' ';

++i;

}

while (i<3);

}

Ausgabe

0 1 2

5.6 Break- und Continue-Anweisungen

Break-Anweisung

#include <iostream>

using namespace std;

int main()

{

for (int i=0; i<10; ++i)

{

if (i==5) break;

cout << i;

}

cout << "Fertig\n";

}

Ausgabe

01234Fertig

#include <iostream>

using namespace std;

int main()

{

for (int i=0; i<2; ++i)

{

cout << i << ": ";

for (int j=0; j<10; ++j)

{

if (j==5) break;

cout << j;

}

cout << '\n';

}

cout << "Fertig\n";

}

Ausgabe

0: 01234

1: 01234

Fertig

Continue-Anweisung

#include <iostream>

using namespace std;

int main()

{

for (int i=0; i<10; ++i)

{

if (i==5) continue;

cout << i;

}

}

Ausgabe

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 37 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

012346789

// Achtung – fehlerhaftes Beispiel

#include <iostream>

using namespace std;

int main()

{

int i = 0;

while (i<10)

{

if (i==5) continue; // Achtung – hier wird eine Endlos-Schleife erzeugt

cout << i;

++i; // Dieses Update wird durch continue uebersprungen

}

}

Ausgabe

01234

5.7 Schleife mit Ausgang in der Mitte

#include <iostream>

using namespace std;

int main()

{

int i = 1;

for (;;)

{

i += 2;

if (i>8) break;

cout << i;

}

}

Ausgabe

357

5.8 „goto“ und Labels

#include <iostream>

using namespace std;

int main()

{

cout << "Vor goto\n";

goto my_label:

cout << "--- wird nicht ausgegeben ---";

my_label:

cout << "Nach Label\n";

}

Ausgabe

Vor goto

Nach Label

Verlassen mehrerer ineinander verschachtelter Schleifen

#include <iostream>

using namespace std;

int main()

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 38 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

{

const int limit = 8;

cout << "Vor Schleife\n";

for (int i=0; i<limit; ++i)

{

cout << "> ";

for (int j=0; j<limit-i; ++j)

{

if (i*j > 10) goto label;

cout << i*j << ' ';

}

cout << " <\n";

}

cout << "Vor Label\n";

label:

cout << "\nNach Label\n";

}

Ausgabe

Vor Schleife

> 0 0 0 0 0 0 0 0 <

> 0 1 2 3 4 5 6 <

> 0 2 4 6 8 10 <

> 0 3 6 9

Nach Label

6 Ein- und Ausgabe

6.1 Ausgabe

„cout“ ist eine globale Variable vom Typ „std::ostream“.

Der Namen „cout“ steht für „char output“, daher für einen Strom von Zeichen vom Typ

„char“.

Der Name „ostream“ steht für „output stream“ (Ausgabe-Strom)

#include <iostream>

#include <string>

int main()

{

int i = 9;

std::cout << i; // Einfache Ausgabe der Variable 'i'

char c = 'H';

std::string s("allo Welt");

double d = 3.14;

std::cout << ' ' << c << s // Verkettete Ausgaben, verteilt auf zwei Zeilen

<< " juhu\n" << d;

}

Ausgabe

9 Hallo Welt juhu

3.14

Manipulatoren

https://en.cppreference.com/w/cpp/io/manip

Folgende 5 Manipulatoren werden wir benutzen.

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 39 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

std::flush – leert den Ausgabe-Puffer

std::endl – fügt einen Zeilenumbruch ein und leert den Ausgabe-Puffer

std::boolapha & std::noboolalpha – beeinflusst die Ausgabe von Bool-Werten

std::setw – definiert die Ausgabe-Breite der nächsten Ausgabe

Das Leeren und Wegschreiben der Puffer-Inhalte nennt man in der Informatik „flushen“

Schieben Sie einfach „std::flush“ in den Stream, und alle Puffer werden geleert und die

Daten verarbeitet.

Alternativ zu „std::flush“ gibt es auch den Manipulator „std::endl“.

Er gibt vor dem Flushen noch einen Zeilenumbruch „\n“ aus.

#include <iostream>

using namespace std;

int main()

{

cout << "Hallo" << flush << ", es geht los!" << endl;

cout << "Wir lernen nun C++" << endl;

}

Ausgabe

Hallo, es geht los!

Wir lernen nun C++

Verwenden Sie diese beiden Manipulatoren nur sehr selten und bewusst. Die

Mit ständigem Flushen können Sie Ausgaben signifikant verlangsamen.

Boolesche Ausgaben

#include <iostream>

using namespace std;

int main()

{

bool bf = false;

bool bt = true;

cout << "false: " << bf << " - " << false << '\n';

cout << "true: " << bt << " - " << true << '\n';

}

Ausgabe

false: 0 - 0

true: 1 - 1

Die textuelle Darstellung von booleschen Werten mit „0“ oder „1“ ist für Einsteiger oft sehr

verwirrend und nicht gut lesbar. Man #include <iostream>

using namespace std;

int main()

{

bool bf = false;

bool bt = true;

cout << "false: " << bf << " - " << false << '\n';

cout << "true: " << bt << " - " << true << '\n';

cout << boolalpha;

cout << "false: " << bf << " - " << false << '\n';

cout << "true: " << bt << " - " << true << '\n';

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 40 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

cout << noboolalpha;

cout << "false: " << bf << " - " << false << '\n';

cout << "true: " << bt << " - " << true << '\n';

}

Ausgabe

false: 0 - 0

true: 1 - 1

false: false - false

true: true - true

false: 0 - 0

true: 1 - 1

Formatierungen

Ausgabebreite

Die Ausgabebreite kann für die nächste Ausgabe mit dem Manipulator std::setw(int)

eingestellt werden – übergeben wird die Breite in Zeichen als „int“.

Achtung: Die gesetzte Ausgabe-Breite gilt nur für die nächste Ausgabe.

Dieser Manipulator ist im Header <iomanip> definiert.

Füllzeichen

Das Default-Füllzeichen für Ausgaben, die kleiner als die eingestellte Ausgabebreite sind,

ist das Leerzeichen.

Das Füllzeichen kann mit der Element-Funktion „char std::cout.fill(char)“ gesetzt werden

– die Element-Funktion gibt dabei das alte Füllzeichen zurück.

#include <iostream>

#include <iomanip>

using namespace std;

int main()

{

cout << 4 << '\n';

cout << setw(2) << 5 << '\n';

cout << setw(3) << 6 << 7 << '\n';

char c = cout.fill('x');

cout << setw(9) << "Hallo" << '\n';

cout.fill('_');

cout << setw(3) << 14 << setw(5) << 15 << '\n';

cout.fill(c);

}

Ausgabe

4

5

67

xxxxHallo

_14___15

6.2 Fehlschläge

Natürlich kann eine Ausgabe auch fehlschlagen.

Im Falle eines Fehlers geht der Ausgabe-Stream in den Status FAIL („fehlerhaft“).

Dieser Status kann mit der Element-Funktion „fail“ abgefragt werden.

Ein Stream im Status FAIL führt keine Aktion mehr durch.

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 41 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Erst wenn der Status wieder auf GOOD gesetzt wurde, funktioniert der Stream wieder

normal.

Die Element-Funktion „clear()“ setzt den Zustand eines Streams wieder auf GOOD

(„fehlerfrei“).

Es wird keinerlei Reparatur durchgeführt.

Es ist ein typischer Anfängerfehler den Aufruf von „clear()“ zu vergessen, oder keine

Reparatur durchzuführen.

// Hinweis - out ist hier ein beliebiger Ausgabe-Stream,

// der zum Beispiel mit einer Datei verbunden sein koennte

out << 42;

if (out.fail()) // Abfrage, ob das Schreiben geklappt hat bzw.

Fehlgeschlagen ist

{

cout << "Schreiben ist fehlgeschlagen.\n";

}

// Hinweis - out ist hier ein beliebiger Ausgabe-Stream,

// der zum Beispiel mit einer Datei verbunden sein koennte

out << 42;

if (out.fail())

{

out.clear(); // Ohne "clear" geht nichts mehr mit "out"

cout << "Fehler";

}

Beispiel 1

// Hier wissen wir am Ende nur, ob das Schreiben komplett funktioniert hat, oder nicht.

// Hinweis - out ist ein Ausgabe-Stream

out << 42 << "Hallo" << 23;

if (out.fail())

{

cout << "Schreiben ist irgendwo fehlgeschlagen.\n";

...

}

Beispiel 2

// Hier wissen wir genau, wo es Probleme gab, wenn es welche gab.

// Hinweis - out ist ein Ausgabe-Stream

out << 42;

if (out.fail())

{

cout << "Schreiben von 42 ist fehlgeschlagen.\n";

...

}

out << "Hallo";

if (out.fail())

{

cout << "Schreiben von \"Hallo\" ist fehlgeschlagen.\n";

...

}

out << 23;

if (out.fail())

{

cout << "Schreiben von 23 ist fehlgeschlagen.\n";

...

}

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 42 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

6.3 Eingabe

„cin“ ist eine globale Variable vom Typ „std::istream“.

Der Namen „cin“ steht für „char input“, daher für einen Eingabe-Strom von Zeichen vom

Typ „char“.

Der Name „istream“ steht für „iutput stream“ (Eingabe-Strom)

// Bitte geben Sie korrekte Werte fuer int, char und double ein.

// Das Programm enthaelt keine Fehlerbehandlung.

#include <iostream>

using namespace std;

int main()

{

int i;

char c;

double d;

cout << "Eingabe int, char, double: ";

cin >> i;

cin >> c >> d; // Verkettung funktioniert auch hier

cout << "i: " << i << '\n'

<< "c: " << c << '\n'

<< "d: " << d << '\n';

}

Mögliche Ein- bzw. Ausgabe

Eingabe int, char, double: 6x3.1415

i: 6

c: x

d: 3.1415

Die Eingabe eines Typs mit dem Eingabe-Operator “>>” endet:

Entweder bei einem Whitespace (Leerzeichen, Tabulator, Zeilenumbruch – also einem

Zeichen, das man „nicht sehen kann“).

Oder bei einem Zeichen, dass nicht mehr zum Typ passt – also zum Beispiel einem

Buchstaben, wenn eine Zahl eingelesen wird.

Ansonsten werden so viele Zeichen gelesen, wie sinnvoll verarbeitet werden können,

daher zu dem einzulesenden Typen passen.

Whitespaces werden dabei immer überlesen.

Daher wäre auch folgender Ablauf des Beispiels möglich:

Mögliche Ein- bzw. Ausgabe

Eingabe int, char, double: 12345 a 2.789

i: 12345

c: x

d: 2.789

Fehlschläge

Einlesen kann eigentlich immer fehlschlagen.

Schlägt die Eingabe fehl, so wird der Eingabe-Stream auf den Status FAIL gesetzt.

Dieser Zustand kann zum Beispiel mit der Element-Funktion „fail()“ abgefragt werden.

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 43 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

#include <iostream>

using namespace std;

int main()

{

int i;

cout << "Eingabe: ";

cin >> i;

if (cin.fail())

{

cout << "Fehler bei Eingabe\n";

return 0;

}

cout << "Eingabe von: " << i << '\n';

}

Mögliche Ein- bzw. Ausgabe

Eingabe: 42

Eingabe von: 42

Mögliche Ein- bzw. Ausgabe

Eingabe: X

Fehler bei Eingabe

Genau genommen bedeutet der Status FAIL, dass nichts gelesen werden konnte – aus

welchen Gründen auch immer.

Stream leeren

// Achtung – fehlerhaftes Beispiel – so nicht machen!

#include <iostream>

using namespace std;

int main()

{

int i;

for (;;)

{

cout << "Bitte geben Sie eine Zahl ein: ";

cin >> i;

if (!cin.fail())break;

cout << "-> Fehlerhafte Eingabe\n";

cin.clear(); // "clear()" ist schon mal sehr gut

} // Das reicht aber leider nicht

cout << "=> Eingabe: " << i << '\n';

}

Mögliche Ein- bzw. Ausgabe

Bitte geben Sie eine Zahl ein: xyz

-> Fehlerhafte Eingabe

Bitte geben Sie eine Zahl ein: -> Fehlerhafte Eingabe

Bitte geben Sie eine Zahl ein: -> Fehlerhafte Eingabe

Bitte geben Sie eine Zahl ein: -> Fehlerhafte Eingabe

Bitte geben Sie eine Zahl ein: -> Fehlerhafte Eingabe

Bitte geben Sie eine Zahl ein: -> Fehlerhafte Eingabe

Bitte geben Sie eine Zahl ein: -> Fehlerhafte Eingabe

...

„ignore()“

Die Element-Funktion „ignore“ bekommt zwei Argumente:

Die Anzahl an Zeichen, die höchstens ignoriert (aus dem Stream entfernt) werden sollen.

Ein Ende-Zeichen, bei dem das Ignorieren gestoppt werden soll. Hierbei wird das Ende-

Zeichen noch mit entfernt.

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 44 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

cin.ignore(numeric_limits<streamsize>::max(), '\n');

Zurück zum Einlesen

#include <iostream>

#include <limits>

using namespace std;

int main()

{

int counter = 1;

int i;

for (;; ++counter)

{

cout << "Bitte geben Sie eine Zahl ein: ";

cin >> i;

if (!cin.fail())break;

cout << "-> Fehlerhafte Eingabe \n";

cin.clear();

cin.ignore(numeric_limits<streamsize>::max(), '\n');

}

cout << "=> Zahl " << i << " nach " << counter << " Versuchen\n";

}

Mögliche Ein- bzw. Ausgabe

Bitte geben Sie eine Zahl ein: a

-> Fehlerhafte Eingabe

Bitte geben Sie eine Zahl ein: b

-> Fehlerhafte Eingabe

Bitte geben Sie eine Zahl ein: 4

=> Zahl 4 nach 3 Versuchen

Programm-Fluss und -Kontrolle

Während der Eingabe verliert das C++ Programm die Kontrolle über den Programmfluss.

Diese kehrt frühestens mit der Benutzer Eingabe von „Return“ zum Programm zurück.

#include <iostream>

using namespace std;

int main()

{

int i1, i2;

cout << "Bitte Eingabe zweier Int-Zahlen: ";

cin >> i1 >> i2;

cout << "i1: " << i1 << "\ni2: " << i2 << '\n';

}

Mögliche Ein- bzw. Ausgabe

Bitte Eingabe zweier Int-Zahlen: 34

67

i1: 34

i2: 67

#include <iostream>

using namespace std;

int main()

{

char c;

cout << "Bitte Eingabe eines Zeichens: ";

cin >> c;

cout << "Eingegeben wurde '" << c << "'\n";

}

Mögliche Ein- bzw. Ausgabe

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 45 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Bitte Eingabe eines Zeichens:

x

Eingegeben wurde 'x'

„Fehlertolerantes Einlesen ist einfach der pure Spaß!“

7 Texte

In C++ werden Texte durch zwei Typen abgedeckt.

Zum einen hat C++ die Zeichenketten aus C geerbt, die dort mit Char-Zeigern verwaltet

werden.

Statt dessen nutzen wir in C++ den Daten-Typ „std::string“

7.1 „std::string“

https://de.cppreference.com/w/cpp/string/basic_string

String-Erzeugung

Es gibt in C++ 16 Arten einen String zu erzeugen.

Vier davon schauen wir uns hier an.

String-Literal

Leer-String

Zeichenkette

String mit n-mal ein Zeichen

String-Literal

"Hallo"s // Ein String-Literal vom Typ "std::string"

"Hallo" // Ein Zeichenketten-Literal – NICHT vom Typ "std::string"

auto s = "Hallo"s; // Eine String-Variable vom Typ "std::string"

Leer-String

string s; // Leer-String

string s1; // Leer-String

string s2{}; // Leer-String

string s3(); // Achtung: Kein String, sondern eine Funktions-Deklaration

Zeichenkette

string s1("C++"); // Runde Klammern - okay

string s2{"C++"}; // Runde Klammern - okay

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 46 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

string s3 = "C++"; // Copy-Initialisation – nicht gut!

string s4 = "C++"s; // Copy-Initialisation mit String-Literal – das ist okay

String mit n-mal ein Zeichen

string s1(4, 'x'); // => "xxxx"

string s2(6, '1'); // => "111111"

string s3{66, 'B'}; // Keine geschweiften Klammern – das ist was anderes

Beispiel

#include <iostream>

#include <string>

using namespace std;

int main()

{

auto s1 = "Hallo Welt"s;

cout << s1 << '\n';

string s2("C++ ist toll");

cout << s2 << '\n';

string s3;

cout << '<' << s3 << ">\n"; // Damit man sieht, dass "s3" leer ist

string s4("C++");

cout << s4 << '\n';

string s5(5, 'A');

cout << s5 << '\n';

}

Ausgabe

Hallo Welt

C++ ist toll

<>

C++

AAAAA

„std::string::size_type“

Der Typ, der die Länge eines Strings beschreiben kann, ist „std::string::size_type“

std::string::size_type

Der Standard schreibt im Falle von „std::string::size_type“ nur vor:

1. Das es diesen Typ-Alias geben muss.

2. Das er groß genug ist, die max. String-Länge aufzunehmen.

3. Und das er auf einen elementaren integralen vorzeichenlosen Datentyp abgebildet

werden muss.

Der Rest ist der Implementierung überlassen.

Länge

#include <iostream>

#include <string>

using namespace std;

int main()

{

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 47 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

string s("C++");

string::size_type len = s.length();

cout << "Laenge: " << len; // => 3

}

Ausgabe

Laenge: 3

#include <iostream>

#include <string>

using namespace std;

int main()

{

string s("Hallo lieber Leser");

string::size_type len1 = s.length(); // Mit explizitem Typ

auto len2 = s.length(); // Mit "auto" Variable

cout << "Laenge: " << len1 << " - " << len2; // => 18 - 18

}

Ausgabe

Laenge: 18 – 18

Ein- und Ausgabe

#include <iostream>

#include <string>

using namespace std;

int main()

{

string s;

cout << "Eingabe: ";

cin >> s;

cout << '"' << s << '"';

}

Mögliche Ein- und Ausgabe:

Eingabe: Hallo Welt

"Hallo"

Komplette Eingabezeile inkl. Whitespaces => Funktion „std::getline“.

Defaultmäßig liest „std::getline“ bis zum abschließenden ‚\n’, daher bis zum Return vom

Benutzer.

Dabei wird das Ende-Zeichen (hier ‚\n’) nicht in den String geschrieben, aber aus dem

Stream entfernt.

#include <iostream>

#include <string>

using namespace std;

int main()

{

string s;

cout << "Eingabe: ";

getline(cin, s);

cout << ">>" << s << "<<\n";

}

Mögliche Ein- und Ausgabe:

Eingabe: Hallo Welt

>>Hallo Welt<<

String-Verkettungen

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 48 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

#include <iostream>

#include <string>

using namespace std;

int main()

{

string s1("Ot");

string s2("to");

string s3(s1 + s2);

cout << s3 << '\n'; // -> Otto

s3 += s1;

cout << s3 << '\n'; // -> OttoOt

s3 += 'x';

cout << s3 << '\n'; // -> OttoOtx

s3 += "-y-z";

cout << s3 << '\n'; // -> OttoOtx-y-z

s3 = s1 + "::" + s2; // (*)

cout << s3 << '\n'; // -> Ot::to

}

Ausgabe

Otto

OttoOt

OttoOtx

OttoOtx-y-z

Ot::to

Vergleiche

#include <iostream>

#include <string>

using namespace std;

int main()

{

string s1("C++");

string s2("APL");

if (s1==s2)

{

cout << s1 << " und " << s2 << " sind gleich\n";

}

else

{

cout << s1 << " und " << s2 << " sind ungleich\n";

}

bool lt = s1<s2;

cout << s1 << " ist " << (lt?"": "nicht ") << "kleiner als " << s2 << '\n';

}

Ausgabe

C++ und APL sind ungleich

C++ ist nicht kleiner als APL

Die Kleiner- und Größer-Relationen werden nur über die Kodierung der Zeichen

ausgewertet.

Es wird daher keine lexikalische Ordnung berücksichtigt.

Index-Zugriff auf einzelne Zeichen

Lesender und schreibender 0-basierter Zugriff auf einzelne Zeichen mit dem Index-Operator

„[ ]“

#include <iostream>

#include <string>

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 49 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

using namespace std;

int main()

{

string s("C++");

cout << s << '\n';

char c = s[0];

cout << c << '\n';

s[s.length()-1] = 'D';

cout << s << '\n';

}

Ausgabe

C++

C

C+D

const string s("C++"); // Konstanter String

char c = s[1]; // Lesen funktioniert

s[0] = 'D'; // Compiler-Fehler, da konstanter String

Achtung: der Zugriff auf ein Zeichen, das nicht existiert, ist nicht erlaubt und erzeugt

„Undefined Behaviour“.

string s("C++");

s[6] = 'X'; // Undefined Behaviour - Zugriff ausserhalb des Strings

Teil-Strings

substr()

Gibt den kompletten Original-String als Kopie zurück.

substr(idx)

Gibt den Teilstring ab dem Index „idx“ inkl. (0-basiert) zurück.

substr(idx, n)

Gibt den Teilstring ab dem Index „idx“ inkl. (0-basiert) mit max. „n“ Zeichen zurück.

#include <iostream>

#include <string>

using namespace std;

int main()

{

string s ("123456789");

cout << s.substr() << '\n';

cout << s.substr(4) << '\n';

cout << s.substr(4, 3) << '\n';

cout << s.substr(7, 3) << '\n';

cout << s.substr(7, 10) << '\n';

}

Ausgabe

123456789

56789

567

89

89

Suchen und Ersetzen

find(Such-Text bzw. Such-Zeichen, pos)

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 50 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Sucht nach dem Vorkommen des Such-Textes oder des Such-Zeichens ab der Postion „pos“ (0-

basiert) und liefert die Postion zurück. Wird der zu suchende Text nicht gefunden, so wird die

Konstanten „std::string::npos“ zurückgegeben. Hierbei sind auch Start-Positionen außerhalb des

Strings erlaubt – in diesem Fall ist die Rückgabe immer „std::string::npos“. Wird keine Position

„pos“ übergeben, so wird der String von Anfang an durchsucht.

rfind(Such-Text bzw. Such-Zeichen, pos)

Alternativ zu „find()“ gibt es die Element-Funktion „rfind()“, die rückwärts sucht – daher am

Text-Ende anfängt und nach vorne vergleicht.

replace(pos, n, Ersatz-String)

Ersetzt im String an der Postion „pos“ (0-basiert) „n“ Zeichen durch den Ersatz-String.

#include <iostream>

#include <string>

using namespace std;

int main()

{

string s("Karl ist toll und Karl ist gut");

for (

string::size_type pos = s.find("Karl");

pos != string::npos;

pos = s.find("Karl", pos))

{

s.replace(pos, 4, "C++");

}

cout << "Besser: \"" << s << "\"\n";

for (

string::size_type pos = s.find(' ');

pos != string::npos;

pos = s.find(' ', pos))

{

s.replace(pos, 1, "__");

}

cout << "Komisch: \"" << s << "\"\n";

}

Ausgabe

Besser: "C++ ist toll und C++ ist gut"

Komisch: "C++__ist__toll__und__C++__ist__gut"

Löschen

erase(pos, n)

Löscht die ‚n‘ Zeichen ab der Position ‚pos‘ (0-basiert) aus dem String.

Wird keine Zeichen-Anzahl ‚n‘ angegeben, wo werden alle Zeichen ab ‚pos‘ inkl. gelöscht

Wird gar kein Parameter übergeben, so wird der komplette String gelöscht.

#include <iostream>

#include <string>

using namespace std;

int main()

{

string s("C++ ist nicht toll");

s.erase(8, 6); // loescht 6 Zeichen ab dem neunten Zeichen inkl.

cout << "-> '" << s << "'\n";

s.erase(3); // loescht alle Zeichen ab dem vierten inkl.

cout << "-> '" << s << "'\n";

s.erase(); // loescht alle Zeichen

cout << "-> '" << s << "'\n";

}

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 51 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Ausgabe

-> 'C++ ist toll'

-> 'C++'

-> ''

7.2 String-Views

Ein String-View ist einfach nur eine nur-lesende Sicht auf eine Zeichenkette oder einen

String.

#include <iostream>

#include <string>

#include <string_view>

using namespace std;

int main()

{

string s("Dies ist ein Text");

// Statt substr mit Kopie lieber String-View

string_view sv1(s);

cout << "String-View: " << sv1 << '\n';

cout << "- Laenge: " << sv1.length() << '\n';

sv1.remove_prefix(5);

sv1.remove_suffix(1);

cout << "String-View: " << sv1 << '\n';

cout << "- Laenge: " << sv1.length() << '\n';

// substr macht keine String-Kopie => viel effizienter

string_view sub = sv1.substr(4, 3);

cout << "Sub String-View: " << sub << '\n';

cout << "- Laenge: " << sub.length() << '\n';

string_view sv2("Arbeiten auch mit Zeichenketten");

cout << "String-View 2: " << sv2 << '\n';

cout << "- Laenge: " << sv2.length() << '\n';

}

Ausgabe

String-View: Dies ist ein Text

- Laenge: 17

String-View: ist ein Tex

- Laenge: 11

Sub String-View: ein

- Laenge: 3

String-View 2: Arbeiten auch mit Zeichenketten

- Laenge: 31

https://en.cppreference.com/w/cpp/string/basic_string_view

7.3 String-Wandlungen

Die Funktion „std::to_string()“ aus dem Header „string“ wandelt die Zahlen-Typen

(Integer- und Fließkomma-Typen) in Strings um.

Die Funktionen „std::stoi()“, „std::stol()“, „std::stod()“ und weitere wandeln Strings in

Integer- bzw. Fließkomma-Typen um. Im Falle von Fehlern (die bei dieser Wandlung

natürlich vorkommen können, werden diese mit Exceptions gemeldet. Außerdem

unterstützen diese Funktionen noch verschiedene Basen und die Möglichkeit, die Anzahl

an geparsten Zeichen zu bekommen.

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 52 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Beispiel 1

#include <iostream>

#include <string>

using namespace std;

int main()

{

string s("42"); // Dies ist die Zeichenkette "42" - kein Integer

// Zum Rechnen benoetigt man die "42" zum Beispiel als "int"

int n1 = stoi(s); // Nun hat man einen "int" zum Rechnen

int n2 = 2*n1;

cout << "2 x " << s << " = " << n2 << '\n';

}

Ausgabe

2 x 42 = 84

Beispiel 2

#include <iostream>

#include <string>

using namespace std;

int main()

{

int n = 123212321; // Dies ist ein "int" mit dem Wert "123212321" - kein Text

// Fuer Text-Operationen benoetigt man einen String

string s = to_string(n); // Nun hat man einen String zum Bearbeiten

for (string::size_type i = 0; i < s.length(); ++i)

{

if (s[i]=='2') s[i]='_';

}

cout << s << '\n';

}

Ausgabe

1_3_1_3_1

http://en.cppreference.com/w/cpp/string/basic_string/to_string

http://en.cppreference.com/w/cpp/string/basic_string/stol

http://en.cppreference.com/w/cpp/string/basic_string/stof

7.4 String-Streams

Eingabe-String-Stream: istringstream

Ausgabe-String-Stream: ostringstream

Der Header für beide Klassen ist „sstream“.

Ganz normale Stream-Klassen.

Der Eingabe-String-Stream liest aus einem String.

Der Ausgabe-String-Stream schreibt in einen String.

#include <iostream>

#include <sstream>

using namespace std;

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 53 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

int main()

{

int i = 42;

double d = 3.14;

ostringstream oss;

oss << i << ' ' << d;

string s(oss.str());

cout << "s: \"" << s << "\"\n"; // -> s: "42 3.14"

cout << "len: " << s.length() << '\n'; // -> len: 7

i = 0;

d = 0.0;

cout << "i: " << i << '\n'; // -> i: 0

cout << "d: " << d << '\n'; // -> d: 0

istringstream iss(s); // (*)

iss >> i >> d;

cout << "s: \"" << s << "\"\n"; // -> s: "42 3.14"

cout << "i: " << i << '\n'; // -> i: 42

cout << "d: " << d << '\n'; // -> d: 3.14

}

Ausgabe

s: "42 3.14"

len: 7

i: 0

d: 0

s: "42 3.14"

i: 42

d: 3.14

Ausgabe-String-Stream:

Mit der Element-Funktion „str()“ kommt man an den Ergebnis-String der Ausgaben.

Eingabe-String-Stream:

Der Eingabe-Stream wird bei der Konstruktion direkt mit dem String initialisiert, aus dem

gelesen werden soll.

#include <iomanip>

#include <iostream>

#include <sstream>

using namespace std;

int main()

{

ostringstream oss;

oss.fill('>');

oss << setw(1) << 1 << " - ";

oss << setw(2) << 2 << " - ";

oss << setw(3) << 3 << " - ";

oss << setw(4) << 4;

string s = oss.str();

cout << "Erzeugter String: " << s << '\n';

}

Ausgabe

Erzeugter String: 1 - >2 - >>3 - >>>4

Wandlungen mit String-Streams

Beispiel 1 mit String-Streams:

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 54 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

#include <iostream>

#include <sstream>

#include <string>

using namespace std;

int main()

{

string s("42"); // Dies ist die Zeichenkette "42" - kein Integer

// Zum Rechnen benoetigt man die "42" zum Beispiel als "int"

int n1;

istringstream iss(s);

iss >> n1; // Nun hat man einen "int" zum Rechnen

int n2 = 2*n1;

cout << "2 x " << s << " = " << n2 << '\n';

}

Ausgabe

2 x 42 = 84

Beispiel 2 mit String-Streams:

#include <iostream>

#include <sstream>

#include <string>

using namespace std;

int main()

{

int n = 123212321; // Dies ist ein "int" mit dem Wert "123212321" - kein Text

// Fuer Text-Operationen benoetigt man einen String

ostringstream oss;

oss << n;

string s(oss.str()); // Nun hat man einen String zum Bearbeiten

for (string::size_type i = 0; i < s.length(); ++i)

{

if (s[i]=='2') s[i]='_';

}

cout << s << '\n';

}

Ausgabe

1_3_1_3_1

7.5 Reguläre Ausdrücke

In der Standard-Bibliothek von C++ gibt es Unterstützung für reguläre Ausdrücke (Regular

Expressions). Reguläre Ausdrücke beschreiben Muster in Texten mit einer einfachen

Sprache. Viele typische String-Probleme, wie zum Beispiel "erkenne alle Texte, die mit

„.doc“ oder „.docx“ enden" oder "erkenne alle Ziffern im Text" lassen sich mit regulären

Ausdrücken leicht umsetzen.

Um die Text-Muster zu beschreiben unterstützt C++ mehrere sehr ähnliche Beschreibungs-

Sprachen. Der Default ist "ECMAScript", der unter anderem hier detailliert beschreiben wird:

http://ecma-international.org/ecma-262/5.1/#sec-15.10 . Da reguläre Ausdrücke dicke

Bücher füllen, sprengen sie den Rahmen des Buchs bei weitem. Stattdessen möchte ich

einfach nur anhand zweier kleiner Beispiele zeigen, wie einfach die regulären Ausdrücke in

C++ zu nutzen sind.

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 55 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Hinweise:

In beiden folgenden Beispielen verwende ich Sprach-Elemente von C++, die erst in

kommenden eingeführt werden. Ignorieren Sie diesen Vorgriff bitte, und kommen Sie

später nochmal in Ruhe zu diesem Kapitel zurück. Ich habe reguläre Ausdrücke trotzdem

hier schon vorgestellt, da sie zum Kontext von Texten und Textbearbeitung gehören.

Um reguläre Ausdrücke in C++ zu nutzen, müssen Sie den Header "regex" einbinden.

Das erste Beispiel zeigt, welche Strings mit dem Muster "ab+c" matchen. Hierbei sagt das

"+" Zeichen hinter dem Zeichen "b", dass "b" mindestens einmal vorkommen muss, aber

auch beliebig häufig vorkommen kann. "ab+c" matcht also alle Texte, die mit einem "a"

anfangen, mit einem "c" enden, und in der Mitte beliebig viele "b"s haben, solange

mindestens ein "b" vorhanden ist.

#include <iostream>

#include <regex>

#include <string>

#include <vector>

using namespace std;

int main()

{

cout << boolalpha;

vector<string> v{ "ac", "abc", "abbc", "abbbc", "abbbbc", "adc", "abdc" };

regex re("ab+c");

for (const string& s : v)

{

cout << s << " --> " << regex_match(s, re) << endl;

}

}

Ausgabe

ac --> false

abc --> true

abbc --> true

abbbc --> true

abbbbc --> true

adc --> false

abdc --> false

Im nächsten Beispiel geht es nicht darum, ob Texte einem entsprechenden Muster

entsprechen, sondern in einem Text alle Vorkommen eines Musters zu finden.

Der reguläre Ausdruck „\d“ steht hierbei für eine beliebige Ziffer (ohne das Backslash

wäre es das Zeichen „d“), und das nachgestellte „+“ wie im ersten Beispiel wieder für ein

beliebig häufiges Vorkommen, solange es mindestens einmal vorkommt. Der reguläre

Ausdruck „\d+“ findet also alle Zahlen.

Mit Iteratoren, die wir erst in ein paar Kapiteln kennenlernen werden, können wir alle

Vorkommen des Musters ablaufen.

„cout << it->str()“ in Zeile (*) gibt also nacheinander alle Zahlen im Text aus.

#include <iostream>

#include <regex>

#include <string>

using namespace std;

int main()

{

regex re("\\d+");

string s("Dies 123 ist ein 4 Text 56 mit 789012 eingestreuten Ziffern 0 wie zum Beispiel

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 56 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

1234.");

sregex_iterator it(cbegin(s), cend(s), re);

sregex_iterator eit;

for (; it != eit; ++it)

{

cout << it->str() << endl; // (*)

}

}

Ausgabe

123

4

56

789012

0

1234

8 STL – Container & Iteratoren

8.1 Einführung

http://www.cplusplus.com/

http://www.cplusplus.com/reference/

http://en.cppreference.com/w/

Container-Vergleich:

Container Vorteil Nachteil

std::array

statisches Array

feste Größe

schneller wahlfreier Zugriff

sehr wenig Speicherbedarf

Lokalität bei Zugriffen

kein Einfügen oder Löschen

(feste Größe)

möglicherweise aufwändiges

Umkopieren (z.B. beim Sortieren)

langsames Suchen

std::vector

dynamisches Array

schneller wahlfreier Zugriff

wenig Speicherbedarf

schnelles Einfügen hinten

Lokalität bei Zugriffen

aufwändiges Einfügen vorne

aufwändiges Löschen vorne

möglicherweise aufwändiges

Umkopieren (z.B. beim Sortieren)

langsames Suchen

std::list

doppelt verkettete Liste

schnelles Einfügen

schnelles Löschen

schnelles „Umkopieren“ (da

nur Zeiger-Änderungen, kein

echtes Kopieren)

kein wahlfreier Zugriff

höherer Speicherbedarf

keine Lokalität bei Zugriffen

langsames Suchen

std::set

std::multiset

„binärer Baum“ ohne bzw.

mit doppelten Elementen

schnelles Suchen

implizite Sortierung

schnelles Einfügen

schnelles Löschen

Elemente werden beim Einfügen

umsortiert, d.h. die Element-

Reihenfolge wird verändert

relativ aufwändiges Löschen und

Einfügen

kein wahlfreier Zugriff

Ordnung (< Operator) notwendig

std::map schnelles Suchen Elemente werden beim Einfügen

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 57 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

std::multimap

„binärer Baum“ für

Schlüsse/Wert Paare ohne

bzw. mit doppelten

Schlüsseln

implizite Sortierung

Abbildung von Schlüssel auf

Wert

schnelles Einfügen

schnelles Löschen

umsortiert, d.h. die Element-

Reihenfolge wird verändert

relativ aufwändiges Löschen und

Einfügen

kein wahlfreier Zugriff

Ordnung (< Operator) notwendig

std::unordered_set

std::unordered_multiset

„Hash-Set“ ohne bzw. mit

doppelten Elementen

sehr schnelles Suchen

schnelles Einfügen

schnelles Löschen

Elemente werden beim Einfügen

umsortiert, d.h. die Element-

Reihenfolge wird verändert

kein wahlfreier Zugriff

Hash-Funktion notwendig

std::unordered_map

std::unordered_multimap

„Hash-Map“ für

Schlüsse/Wert Paare ohne

bzw. mit doppelten

Schlüsseln

sehr schnelles Suchen

Abbildung von Schlüssel auf

Wert

schnelles Einfügen

schnelles Löschen

Elemente werden beim Einfügen

umsortiert, d.h. die Element-

Reihenfolge wird verändert

kein wahlfreier Zugriff

Hash-Funktion notwendig

8.2 Iteratoren

Allgemeines Konzept um auf die Elemente eines Objekts sequentiell zugreifen zu können,

ohne die zugrundlegende Repräsentation zu kennen.

#include <iostream>

#include <vector>

using namespace std;

int main()

{

vector<int> v{1, 3, 2, 4};

// Nutzung eines Non-Const-Iterators – schreibende und lesende Zugriffe erlaubt

for (vector<int>::iterator it=begin(v); it!=end(v); ++it)

{

cout << *it << ' '; // 1 3 2 4

*it += 4; // Schreibender Zugriff

}

cout << endl;

// Nutzung eines Const-Iterators – nur lesende Zugriffe erlaubt

for (vector<int>::const_iterator it=begin(v); it!=end(v); ++it)

{

cout << *it << ' '; // 5 7 6 8

}

}

Ausgabe

1 3 2 4

5 7 6 8

Sie abstrahieren vom zugrunde liegenden Container.

Neben dem Vektor funktionieren sie also auch problemlos für Listen und andere STL

Container:

#include <iostream>

#include <list>

using namespace std;

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 58 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

int main()

{

list<int> l { 1, 3, 2, 4 };

// Nutzung eines Non-Const-Iterators – schreibende und lesende Zugriffe erlaubt

for (list<int>::iterator it=begin(l); it!=end(l); ++it)

{

cout << *it << ' '; // 1 3 2 4

*it += 4; // Schreibender Zugriff

}

cout << endl;

// Nutzung eines Const-Iterators – nur lesende Zugriffe erlaubt

for (list<int>::const_iterator it=begin(l); it!=end(l); ++it)

{

cout << *it << ' '; // 5 7 6 8

}

}

Ausgabe

1 3 2 4

5 7 6 8

Neben dem Zugriff auf das Element mit dem auf den Iterator angewandten Dereferen-

zierungs-Operator „*“ gibt es noch eine zweite Möglichkeit für Element-Zugriffe mit dem

Pfeil-Operator „->“.

#include <iostream>

#include <list>

#include <string>

using namespace std;

int main()

{

list<string> l;

l.push_back("Backe");

l.push_back("backe");

l.push_back("Kuchen");

// 1. Schleife – Ausgabe der Strings

for (list<string>::const_iterator it=begin(l); it!=end(l); ++it)

{

cout << *it << ' ';

}

cout << endl;

// 2. Schleife – Ausgabe der String-Laengen mit dem Punkt-Operator '.'

for (list<string>::const_iterator it=begin(l); it!=end(l); ++it)

{

cout << (*it).length() << ' ';

}

cout << endl;

// 3. Schleife – Ausgabe der String-Laengen mit dem Pfeil-Operator '->'

for (list<string>::const_iterator it=begin(l); it!=end(l); ++it)

{

cout << it->length() << ' ';

}

cout << endl;

}

Ausgabe

Backe backe Kuchen

5 5 6

5 5 6

Achtung – greifen Sie niemals über einen Ende-Iterator eines Containers auf das Element

zu. Es existiert nicht, da der Ende-Iterator ja nur auf ein virtuelles Element zeigt. Das

Verhalten Ihres Programms ist beim Dereferenzieren des Ende-Iterators undefiniert.

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 59 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

8.2.1 Iteratoren mit „auto“

#include <iostream>

#include <list>

using namespace std;

int main()

{

list<int> l;

l.push_back(1);

l.push_back(3);

l.push_back(2);

l.push_back(4);

// Nutzung von "auto" fuer die Iterator Variablen-Definition

for (auto it=begin(l); it!=end(l); ++it)

{

cout << *it << ' '; // 1 3 2 4

}

}

Ausgabe

1 3 2 4

8.2.2 Range basierte For-Schleife

Alternative zu Iterator-Schleife: Range-basierte For-Schleife

Arbeitet intern mit Iteratoren, nur einfachere Syntax

#include <iostream>

#include <vector>

using namespace std;

int main()

{

vector<int> v;

v.push_back(2);

v.push_back(4);

v.push_back(8);

v.push_back(1);

for (int value : v)

{

cout << value << ' ';

}

cout << endl;

}

Ausgabe

2 4 8 1

8.3 Vektoren

Ist ein dynamisches Array.

Sequentieller Container:

- Die Elemente liegen in einer definierten Reihenfolge (sequentiell) vor.

- Die Reihenfolge wird nur durch die Bestückung von außen definiert.

Passt sich dynamisch in der Größe an:

- Der intern verwendetet Speicherplatz wird in Schritten vergrößert.

- Eine solche Reallocation kostet Zeit, u.a. da die Elemente umkopiert werden müssen.

- Man kann die Größe des internen Speicherplatzes des Vektors vordefinieren.

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 60 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

- Achtung – die Größe des internen Speicherplatzes wird nie reduziert.

Die Elemente liegen bündig im Speicher hintereinander.

Unterstützt Random-Access Iteratoren.

Haupt-Vorteile:

Schneller wahlfreier Zugriff, d.h. direkter Zugriff auf ein Element über Index.

Wenig Speicherbedarf, da keine Verwaltuns-Informationen pro Element notwendig sind.

Unterstützt Random-Access Iteratoren.

Schnelles Hinzufügen neuer Elemente am Ende des Vektors – solange die Größe des

internen Speicherplatzes nicht überschritten wird (s.o.).

Schnelles Löschen von Elementen am Ende des Vektors.

Haupt-Nachteile:

Langsames Hinzufügen neuer und Löschen bestehender Elemente in der Mitte und vor

allem am Anfang des Vektors.

Die Vergrößerung des internen Speicherplatzes läuft in Schritten mit Kopieren ab – dies

kann im Einzelfall ein Problem sein. Näheres hierzu finden Sie in der Literatur.

Langsames Suchen, da alle Elemente von vorne nach hinten gecheckt werden müssen

(lineare Suche). Ein doppelt so großer Vektor führt im Schnitt zu einer doppelt so großen

Suchzeit. Die Zeit-Komplexität ist O(n).

Beispiel:

#include <iostream>

#include <vector>

using namespace std;

int main()

{

vector<int> v{ 1, 3, 2 }; // Ein Vector nur fuer int Objekte

v.push_back(4);

v.push_back(1);

cout "Anzahl Elemente im Vektor: " << v.size() << endl;

for (vector<int>::size_type i=0; i<v.size(); ++i)

{

cout << v[i] << ' '; // 1 3 2 4 1

}

}

Ausgabe

Anzahl Elemente im Vektor: 5

1 3 2 4 1

Beispiel:

#include <iostream>

#include <string>

#include <vector>

using namespace std;

int main()

{

vector<string> v; // Ein Vector nur fuer String Objekte

v.push_back("Axel");

v.push_back("Max");

v.push_back("Tim");

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 61 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

v.push_back("Bernd");

for (vector<string>::size_type i=0; i<v.size(); )

{

cout << v[i++];

if (i != v.size())

{

cout << ", ";

}

}

}

Ausgabe

Axel, Max, Tim, Bernd

Beispiel:

#include <iostream>

#include <string>

#include <vector>

using namespace std;

int main()

{

vector<string> v;

v.push_back("Axel");

v.push_back("Max");

v.push_back("Tim");

v.push_back("Bernd");

for (auto i=0; i<v.size(); ) // Falls Sie hier eine Warnung bekommen

{ // -> Erklaerung im Text unten

cout << v[i++];

if (i != v.size())

{

cout << ", ";

}

}

cout << endl;

}

Ausgabe

Axel, Max, Tim, Bernd

Header:

<vector>

Beispiele von Konstruktoren:

vector()

Erzeugt einen leeren Vektor, d.h. einen Vektor mit null Elementen.

vector(size_type n)

Erzeugt einen Vektor mit „n“ Elementen, die alle mit dem Standard-Konstruktor erzeugt

wurden. Bei elementaren Daten-Typen ist hier eine Null-Initialisierung sichergestellt.

Beispiele von Element-Funktionen des Vektors:

bool empty() const

Gibt zurück, ob der Vektor leer ist.

size_type size() const

Gibt die Größe des Vektors zurück, d.h. die Anzahl an Elementen im Vektor. Diese

Funktion gibt nicht die Größe des internen Speicherplatzes zurück.

T& operator[ ](size_type n) bzw. const T& operator[ ](size_type n) const

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 62 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Gibt das n-te Element (‚0‘-basiert) als Referenz zurück.

void push_back(const T& t)

Fügt eine Kopie des Elements „t“ am Ende des Vektors ein.

void pop_back()

Löscht das letzte Element des Vektors.

T& front() bzw. const T& front() const

Gibt das erste Element des Vektors als Referenz zurück.

T& back() bzw. const T& back() const

Gibt das letzte Element des Vektors als Referenz zurück.

void clear()

Löscht den Vektor.

void swap(vector& v)

Vertauscht den Inhalt der Vektoren miteinander.

iterator erase(iterator it)

Löscht das Element, auf das der Lösch-Iterator „it“ zeigt, aus dem Vektor. Die Funktion

gibt einen Iterator auf das Element nach dem Gelöschten zurück.

void erase(iterator first, iterator last)

Löscht die Elemente, die durch die Iteratoren „first“ (inkl) und „last“ (exkl.) bestimmt

werden, aus dem Vektor. Die Funktion gibt nichts zurück.

iterator begin() bzw. const_iterator begin() const

Gibt den (Const) Start-Iterator für den Vektor zurück.

iterator end() bzw. const_iterator end() const

Gibt den (Const) End-Iterator für den Vektor zurück.

iterator rbegin() bzw. const_iterator rbegin() const

Gibt den (Const) Start-Rückwärts-Iterator für den Vektor zurück.

iterator rend() bzw. const_iterator rend() const

Gibt den (Const) End-Rückwärts-Iterator für den Vektor zurück.

const_iterator cbegin() const

Gibt immer den Const-Start-Iterator für den Vektor zurück. (ISO C++11)

const_iterator cend() const

Gibt immer den Const-End-Iterator für den Vektor zurück. (ISO C++11)

const_iterator crbegin() const

Gibt immer den Const-Start-Rückwärts-Iterator für den Vektor zurück. (ISO C++11)

const_iterator crend() const

Gibt immer den Const-End-Rückwärts-Iterator für den Vektor zurück. (ISO C++11)

Beispiel:

#include <iostream>

#include <vector>

using namespace std;

int main()

{

vector<int> v1;

cout << "v1: " << v1.size() << " -> empty? " << v1.empty() << '\n';

vector<int> v2(7);

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 63 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

cout << "v2: " << v2.size() << " -> empty? " << v2.empty() << '\n';

v1.push_back(1);

v1.push_back(2);

v1.push_back(3);

v1.push_back(4);

v1.push_back(5);

cout << "v1: " << v1.size() << " -> empty? " << v1.empty() << '\n';

cout << "v1 front/back: " << v1.front() << '/' << v1.back() << '\n';

v1.pop_back();

cout << "v1: " << v1.size() << " -> empty? " << v1.empty() << '\n';

v1[0] = 11;

cout << "v1: front/back: " << v1.front() << '/' << v1.back() << '\n';

cout << "v1: ";

for (vector<int>::const_iterator it = v1.begin(); it!=v1.end(); ++it)

{

cout << *it << ' ';

}

cout << '\n';

v1.swap(v2);

cout << "v1: " << v1.size() << " -> empty? " << v1.empty() << '\n';

cout << "v2: " << v2.size() << " -> empty? " << v2.empty() << '\n';

cout << "v1: ";

for (vector<int>::const_iterator it = v1.begin(); it!=v1.end(); ++it)

{

cout << *it << ' ';

}

cout << '\n';

v1.clear();

cout << "v1: " << v1.size() << " -> empty? " << v1.empty() << '\n';

}

Ausgabe

v1: 0 -> empty? 1

v2: 7 -> empty? 0

v1: 5 -> empty? 0

v1 front/back: 1/5

v1: 4 -> empty? 0

v1: front/back: 11/4

v1: 11 2 3 4

v1: 7 -> empty? 0

v2: 4 -> empty? 0

v1: 0 0 0 0 0 0 0

v1: 0 -> empty? 1

8.4 Listen

Kurz-Beschreibung:

Doppelt verkettete Liste.

Sequentieller Container.

Paßt sich dynamisch in der Größe an.

Die Elemente liegen ohne Ordnung im Speicher.

Unterstützt Bi-Directionale Iteratoren.

Haupt-Vorteile:

Schnelles Einfügen und Löschen an jeder beliebigen Position, da nur die interne

Verzeigerung umgebaut werden muß. Achtung – Zugriffe innerhalb der Liste geschehen

niemals über Indices, sondern nur über Iteratoren oder Algorithmen.

Schnelles Sortieren, da nur die interne Verzeigerung umgebaut werden muß und keine

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 64 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Elemente kopiert werden müssen (Zumindest bei großen Elementen, bei denen Kopier-

Aktionen ins Gewicht fallen. Bei kleinen Elementen kann das Sortieren mit Kopieren wie

z.B. im Vektor schneller sein.)

Weitere schnelle Operationen aufgrund der Möglichkeit der Verzeigerung wie z.B.

„merge“, „splice“ oder „unique“.

Haupt-Nachteile:

Kein Wahlfreier Zugriff.

Zugriff innerhalb der Liste nur über Iteratoren.

Die Elemente liegen nicht bündig im Speicher.

Höherer Speicherbedarf, da jedes Element zusätzliche interne Verwaltungs-Informationen

benötigt.

Langsames Suchen, da alle Elemente von vorne nach hinten gecheckt werden müssen

(lineare Suche). Eine doppelt so große Liste führt im Schnitt zu einer doppelt so großen

Suchzeit. Die Zeit-Komplexität ist O(n).

Beispiel:

#include <iostream>

#include <list>

using namespace std;

int main()

{

list<int> l; // Eine Liste nur fuer int Objekte

l.push_back(1);

l.push_back(2);

l.push_back(3);

list<int>::size_type count = l.size();

cout "Anzahl Elemente in der Liste: " << count << endl;

}

Ausgabe

Anzahl Elemente in der Liste: 3

Header:

<list>

Beispiele von Konstruktoren:

list()

Erzeugt eine leere Liste, d.h. eine Liste mit null Elementen.

Beispiele von Element-Funktionen der Liste:

bool empty() const

Gibt zurück, ob die Liste leer ist.

size_type size() const

Gibt die Größe der Liste zurück, d.h. die Anzahl an Elementen in der Liste.

void push_front(const T& t)

Fügt eine Kopie des Elements „t“ am Anfang der Liste ein.

void push_back(const T& t)

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 65 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Fügt eine Kopie des Elements „t“ am Ende der Liste ein.

void pop_front()

Löscht das erste Element der Liste.

void pop_back()

Löscht das letzte Element der Liste.

T& front() bzw. const T& front() const

Gibt das erste Element der Liste als Referenz zurück.

T& back() bzw. const T& back() const

Gibt das letzte Element der Liste als Referenz zurück.

void clear()

Löscht die Liste.

void swap(list& l)

Vertauscht den Inhalt der Listen miteinander.

iterator erase(iterator it)

Löscht das Element, auf das der Lösch-Iterator „it“ zeigt, aus der Liste. Die Funktion gibt

einen Iterator auf das Element nach dem Gelöschten zurück. Achtung – der übergebene

Lösch-Iterator ist nach dem Löschen nicht mehr gültig.

void erase(iterator first, iterator last)

Löscht die Elemente, die durch die Iteratoren „first“ (inkl) und „last“ (exkl.) bestimmt

werden, aus der Liste. Die Funktion gibt nichts zurück.

iterator begin() bzw. const_iterator begin() const

Gibt den (Const) Start-Iterator für die Liste zurück.

iterator end() bzw. const_iterator end() const

Gibt den (Const) End-Iterator für die Liste zurück.

iterator rbegin() bzw. const_iterator rbegin() const

Gibt den (Const) Start-Rückwärts-Iterator für die Liste zurück.

iterator rend() bzw. const_iterator rend() const

Gibt den (Const) End-Rückwärts-Iterator für die Liste zurück.

const_iterator cbegin() const

Gibt immer den Const-Start-Iterator für die Liste zurück. (ISO C++11)

const_iterator cend() const

Gibt immer den Const-End-Iterator für die Liste zurück. (ISO C++11)

const_iterator crbegin() const

Gibt immer den Const-Start-Rückwärts-Iterator für die Liste zurück. (ISO C++11)

const_iterator crend() const

Gibt immer den Const-End-Rückwärts-Iterator für die Liste zurück. (ISO C++11)

void sort()

Sortiert die Liste über den Kleiner-Operator „<“ der Elemente. Achtung – für die Liste ist

der Sort-Algorithmus nicht anwendbar. U.a. deshalb ist diese Funktionalität als spezielle

Element-Funktion in der Listen-Klasse implementiert.

Beispiel:

#include <iostream>

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 66 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

#include <list>

using namespace std;

int main()

{

list<int> l;

cout << l.size() << " -> empty? " << l.empty() << '\n';

l.push_back(10);

l.push_back(20);

l.push_back(30);

l.push_front(2);

l.push_front(1);

cout << l.size() << " -> empty? " << l.empty() << '\n';

for (list<int>::const_iterator it = l.begin(); it!=l.end(); ++it)

{

cout << *it << ' ';

}

cout << '\n';

cout << "front/back: " << l.front() << '/' << l.back() << '\n';

l.pop_front();

l.pop_back();

cout << l.size() << " -> empty? " << l.empty() << '\n';

cout << "front/back: " << l.front() << '/' << l.back() << '\n';

l.clear();

cout << l.size() << " -> empty? " << l.empty() << '\n';

l.push_back(5);

l.push_back(8);

l.push_back(3);

l.push_back(1);

l.push_back(4);

for (list<int>::const_iterator it = l.begin(); it!=l.end(); ++it)

{

cout << *it << ' ';

}

cout << '\n';

l.sort();

for (list<int>::const_iterator it = l.begin(); it!=l.end(); ++it)

{

cout << *it << ' ';

}

cout << '\n';

}

Ausgabe

0 -> empty? 1

5 -> empty? 0

1 2 10 20 30

front/back: 1/30

3 -> empty? 0

front/back: 2/20

0 -> empty? 1

5 8 3 1 4

1 3 4 5 8

8.5 Arrays

Array fester Container – wobei die Größe zur Compile-Zeit bekannt sein muss. Da das Array

im Gegensatz zum Vektor eine feste Größe hat, ist er bzgl. Speicherverbrauch und

Performance noch etwas besser als der Vektor.

#include <iostream>

#include <array>

using namespace std;

int main()

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 67 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

{

array<int, 2> a1;

cout << "size: " << a1.size() << '\n';

cout << "a1[0]: " << a1[0] << '\n';

cout << "a1[1]: " << a1[1] << '\n';

array<int, 4> a2 = { 11, 22, 33 };

cout << "size: " << a2.size() << '\n';

cout << "a2[0]: " << a2[0] << '\n';

cout << "a2[1]: " << a2[1] << '\n';

cout << "a2[2]: " << a2[2] << '\n';

cout << "a2[3]: " << a2[3] << '\n';

array<int, 4>::const_iterator it = a2.begin(); // Hier ist auto auch wieder eine

Alternative

array<int, 4>::const_iterator eit = a2.end();

for (; it!=eit; ++it)

{

cout << *it << ' ';

}

}

Mögliche Ausgabe

size: 2

a1[0]: -858993460

a1[1]: -858993460

size: 4

a2[0]: 11

a2[1]: 22

a2[2]: 33

a2[3]: 0

11 22 33 0

8.6 Sets

Kurz-Beschreibung:

Geordneter Container – Ordnung über den Kleiner-Operator „<“ der Elemente im Set.

Keine mehrfachen Elemente – für diese Anforderung gibt es Multi-Sets in der STL.

Paßt sich dynamisch in der Größe an.

Unterstützt Bi-Directionale Iteratoren.

Wird heutzutage typischerweise mit ausbalancierten binären Rot-Schwarz Bäumen

implementiert.

Haupt-Vorteile:

Elemente liegen sortiert vor.

Schnelles Suchen, da der Container mit binärer Suche durchsucht werden kann. Doppelt

soviele Elemente im Container führen nur zu einem weiteren Suchschritt. Die Zeit-

Komplexität ist O(log n).

Kein Element kann mehrfach im Container vorhanden sein.

Haupt-Nachteile:

Kein Wahlfreier Zugriff – Zugriff nur über Iteratoren.

Aufwändiges (relativ langsames) Einfügen und Löschen.

Die Elemente liegen nicht bündig im Speicher.

Höherer Speicherbedarf, da jedes Element zusätzliche interne Verwaltungs-Informationen

benötigt.

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 68 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

#include <iostream>

#include <set>

using namespace std;

int main()

{

set<int> s; // Ein Set von Int-Elementen

s.insert(4); // Einfuegen der Int-Elemente erfolgt unsortiert

s.insert(1);

s.insert(6);

s.insert(3);

s.insert(2);

s.insert(5);

// Ausgabe der Int-Elemente erfolgt sortiert

for (set<int>::const_iterator it=s.begin(); it!=s.end(); ++it)

{

cout << *it << ' ';

}

cout << endl;

}

Ausgabe

1 2 3 4 5 6

Beispiel

#include <iostream>

#include <set>

#include <string>

using namespace std;

int main()

{

set<string> s;

s.insert("horst");

s.insert("wilhelmine");

s.insert("alexander");

s.insert("detlef");

for (set<string>::const_iterator it=s.begin(); it!=s.end(); ++it)

{

cout << *it << '\n';

}

}

Mögliche Ausgabe – abhängig von der Zeichenkodierung Ihrer Plattform

alexander

detlef

horst

wilhelmine

Beispiel

#include <iostream>

#include <set>

using namespace std;

int main()

{

set<char> s;

s.insert('a');

s.insert('A');

s.insert('b');

s.insert('B');

s.insert('c');

s.insert('C');

for (set<char>::const_iterator it=s.begin(); it!=s.end(); ++it)

{

cout << *it << ' ';

}

cout << endl;

}

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 69 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Mögliche Ausgabe – abhängig von der Zeichenkodierung Ihrer Plattform

A B C a b c

Beispiel:

#include <iostream>

#include <set>

using namespace std;

int main()

{

set<int> s;

s.insert(1); // '1' zum ersten Mal einfuegen

s.insert(1); // '1' wieder einfuegen -> wird ignoriert

s.insert(2);

s.insert(1); // und nochmal die '1' -> wird wieder ignoriert

cout << "Anzahl Element im Set: " << s.size() << endl;

for (set<int>::const_iterator it=s.begin(); it!=s.end(); ++it)

{

cout << *it << ' ';

}

cout << endl;

}

Ausgabe

Anzahl Element im Set: 2

1 2

Beispiel:

#include <iostream>

#include <set>

#include <string>

using namespace std;

class StringLengthCmp

{

public:

bool operator()(const string& s1, const string& s2)

{

return s1.length() < s2.length();

}

};

int main()

{

set<string, StringLengthCmp> s;

s.insert("aa");

s.insert("bbbb");

s.insert("c");

s.insert("ddd");

for (auto it=s.begin(); it!=s.end(); ++it)

{

cout << *it << '\n';

}

}

Ausgabe

c

aa

ddd

bbbb

Suchen im Set

Elemente vs. Zugriffe Sequentieller Container Binärer-Baum

10 Elemente 5------- 4-------

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 70 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

100 Elemente 50------- 7-------

1.000 Elemente 500------- 10-------

1.000.000 Elemente 500.000------- 20-------

1.000.000.000 Elemente 500.000.000------- 30-------

#include <iostream>

#include <set>

using namespace std;

int main()

{

set<int> primes;

primes.insert(1);

primes.insert(2);

primes.insert(3);

primes.insert(5);

primes.insert(7);

primes.insert(11);

set<int>::const_iterator fit = primes.find(3); // '3' ist vorhanden

if (fit!=primes.end()) // -> gibt Iterator auf die 3 zurueck

{

cout << *fit << " ist im Set drin" << endl;

}

else

{

cout << "3 ist NICHT im Set drin" << endl;

}

fit = primes.find(4); // '4' ist nicht vorhanden

if (fit!=primes.end()) // -> gibt Ende-Iterator zurueck

{

cout << *fit << " ist im Set drin" << endl;

}

else

{

cout << "4 ist NICHT im Set drin" << endl;

}

}

Ausgabe

3 ist im Set drin

4 ist NICHT im Set drin

Header:

<set>

Beispiele von Konstruktoren:

set()

Erzeugt ein leeres Set, d.h. ein Set mit null Elementen.

Beispiele von Element-Funktionen des Sets:

bool empty() const

Gibt zurück, ob das Set leer ist.

size_type size() const

Gibt die Größe des Sets zurück, d.h. die Anzahl an Elementen im Set.

void insert(const T& t)

Fügt eine Kopie des Elements „t“ in das Set ein. Ist das Element – bezogen auf den

Kleiner-Operator „<“.

void clear()

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 71 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Löscht das Set.

size_type count(const T& t) const

Gibt die Anzahl an Elementen „t“ zurück – bei einem Set ist dies immer „0“ oder „1“. Geht

es nur um die Existenz eines Elements ohne Zugriff, so ist diese Element-Funktion

gegenüber der Element-Funktion „find“ vorzuziehen.

iterator find(const T& t) bzw. const_iterator find(const T& t) const

Sucht das Element „t“ im Set – mit dem Kleiner-Operator „<“. Wird das Element

gefunden, so wird ein Iterator darauf zurückgegeben. Ist das Element nicht im Set

enthalten, so wird der End-Iterator zurückgegeben.

iterator lower_bound(const T& t)

Liefert zu dem Paramter „t“ einen Iterator auf ein exakt passendes Element, oder wenn

dieses nicht existert, einen Iterator auf das nächste Element (dies kann auch der End-

Iterator sein).

size_type erase(const T& t)

Löscht das Element „t“ aus dem Set. Ist das Element „t“ nicht im Set enthalten, so

passiert nichts. Die Funktion gibt zurück, wieviele Elemente „t“ gelöscht wurden – bei

einem Set ist dies immer „0“ oder „1“.

void erase(iterator it)

Löscht das Element, auf das der Lösch-Iterator „it“ zeigt, aus dem Set. Die Funktion gibt

einen Iterator auf das Element nach dem Gelöschten zurück. Achtung – der übergebene

Lösch-Iterator ist nach dem Löschen nicht mehr gültig.

void erase(iterator first, iterator last)

Löscht die Elemente, die durch die Iteratoren „first“ (inkl) und „last“ (exkl.) bestimmt

werden, aus dem set. Die Funktion gibt nichts zurück.

void swap(set& s)

Vertauscht den Inhalt der Sets miteinander.

iterator begin() bzw. const_iterator begin() const

Gibt den (Const) Start-Iterator für das Set zurück.

iterator end() bzw. const_iterator end() const

Gibt den (Const) End-Iterator für das Set zurück.

iterator rbegin() bzw. const_iterator rbegin() const

Gibt den (Const) Start-Rückwärts-Iterator für das Set zurück.

iterator rend() bzw. const_iterator rend() const

Gibt den (Const) End-Rückwärts-Iterator für das Set zurück.

const_iterator cbegin() const

Gibt immer den Const-Start-Iterator für das Set zurück. (ISO C++11)

const_iterator cend() const

Gibt immer den Const-End-Iterator für das Set zurück. (ISO C++11)

const_iterator crbegin() const

Gibt immer den Const-Start-Rückwärts-Iterator für das Set zurück. (ISO C++11)

const_iterator crend() const

Gibt immer den Const-End-Rückwärts-Iterator für das Set zurück. (ISO C++11)

Beispiel:

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 72 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

#include <iostream>

#include <set>

using namespace std;

int main()

{

set<int> s;

cout << s.size() << " -> empty? " << s.empty() << '\n';

s.insert(4);

s.insert(3);

s.insert(5);

s.insert(1);

s.insert(2);

cout << s.size() << " -> empty? " << s.empty() << '\n';

for (set<int>::const_iterator it = s.begin(); it!=s.end(); ++it)

{

cout << *it << ' ';

}

cout << '\n';

s.insert(1);

s.insert(2);

cout << s.size() << " -> empty? " << s.empty() << '\n';

for (int i=4; i<8; ++i)

{

set<int>::const_iterator it = s.find(i);

if (it==s.end())

{

cout << "Element " << i << " ist nicht vorhanden.\n";

}

else

{

cout << "Element " << i << " ist vorhanden: " << *it << '\n';

}

}

s.erase(2);

set<int>::iterator it = s.find(4);

s.erase(it);

cout << s.size() << " -> empty? " << s.empty() << '\n';

for (set<int>::const_iterator it = s.begin(); it!=s.end(); ++it)

{

cout << *it << ' ';

}

cout << '\n';

s.clear();

cout << s.size() << " -> empty? " << s.empty() << '\n';

}

Ausgabe

0 -> empty? 1

5 -> empty? 0

1 2 3 4 5

5 -> empty? 0

Element 4 ist vorhanden: 4

Element 5 ist vorhanden: 5

Element 6 ist nicht vorhanden.

Element 7 ist nicht vorhanden.

3 -> empty? 0

1 3 5

0 -> empty? 1

8.7 Unordered-Set

Im Prinzip ein Set wie „set“ mit zum größten Teil der identischen Schnittstelle.

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 73 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Kurz-Beschreibung:

Ungeordneter Container

Keine mehrfachen Elemente – für diese Anforderung gibt es Multi-Sets in der STL.

Passt sich dynamisch in der Größe an.

Unterstützt Bi-Directionale Iteratoren.

Wird als Hash-Container implementiert

Haupt-Vorteile:

Extrem schnelles Suchen. Zugriff konstant, unabhängig von der Anzahl der Elemente im

Container

Kein Element kann mehrfach im Container vorhanden sein.

Haupt-Nachteile:

Kein Wahlfreier Zugriff – Zugriff nur über Iteratoren.

Die Elemente liegen nicht bündig im Speicher.

Höherer Speicherbedarf durch

#include <iostream>

#include <unordered_set>

using namespace std;

int main()

{

unordered_set<int> s;

s.insert(4);

s.insert(1);

s.insert(6);

s.insert(3);

s.insert(2);

s.insert(5);

// Ausgabe der Int-Elemente erfolgt sortiert

for (unordered_set<int>::const_iterator it=cbegin(s); it!=cend(s); ++it)

{

cout << *it << ' ';

}

cout << endl;

}

Mögliche Ausgabe (Reihenfolge unspezifziert)

5 1 3 6 2 4

Suchen im Unordered-Set

Elemente vs. Zugriffe Sequentiell Baum Hash

10 Elemente 5------- 4------- 2

100 Elemente 50------- 7------- 2

1.000 Elemente 500------- 10------- 2

1.000.000 Elemente 500.000------- 20------- 2

1.000.000.000

Elemente

500.000.000------- 30------- 2

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 74 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

8.8 Maps

Kurz-Beschreibung:

Assoziativer Container, d.h. der Container implementiert eine Schlüssel-Wert Beziehung.

Geordneter Container – Ordnung über den Kleiner-Operator „<“ der Schlüssel.

Keine mehrfachen Schlüssel – für diese Anforderung gibt es Multi-Maps in der STL.

Paßt sich dynamisch in der Größe an.

Unterstützt Bi-Directionale Iteratoren.

Wird heutzutage typischerweise mit ausbalancierten binären Rot-Schwarz Bäumen

implementiert.

Haupt-Vorteile:

Elemente liegen sortiert vor – sortiert nach dem Schlüssel.

Schnelles Suchen nach dem Schlüssel, da der Container mit binärer Suche durchsucht

werden kann. Doppelt soviele Elemente im Container führen nur zu einem weiteren

Suchschritt. Die Zeit-Komplexität ist O(log n).

Kein Schlüssel kann mehrfach im Container vorhanden sein.

Haupt-Nachteile:

Kein Wahlfreier Zugriff – Zugriff nur über Iteratoren.

Aufwändiges (relativ langsames) Einfügen und Löschen.

Die Schlüssel/Wert-Paare liegen nicht bündig im Speicher.

Höherer Speicherbedarf, da jedes Schlüssel/Wert-Paar zusätzliche interne Verwaltungs-

Informationen benötigt.

#include <iostream>

#include <map>

#include <string>

using namespace std;

int main()

{

map<int, string> m;

m.insert(map<int, string>::value_type(2, "Heinrich"));

m.insert(map<int, string>::value_type(3, "Ede"));

m.insert(map<int, string>::value_type(4, "Ansgar"));

m.insert(map<int, string>::value_type(1, "Willi"));

m.insert(map<int, string>::value_type(5, "Tom"));

// Direkte Nutzung von "it->first" und "it->second"

for (map<int, string>::const_iterator it=m.begin(); it!=m.end(); ++it)

{

cout << it->first << ' ' << it->second << '\n';

}

cout << endl;

// Umweg ueber Schluessel- und Wert-Variable "key" und "value"

for (map<int, string>::const_iterator it=m.begin(); it!=m.end(); ++it)

{

int key = it->first;

string value(it->second);

cout << key << " -> " << value << '\n';

}

}

Ausgabe

1 Willi

2 Heinrich

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 75 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

3 Ede

4 Ansgar

5 Tom

1 -> Willi

2 -> Heinrich

3 -> Ede

4 -> Ansgar

5 -> Tom

Beispiel

#include <iostream>

#include <map>

#include <string>

using namespace std;

int main()

{

map<int, string> m {{ 2, "Heinrich" }, { 3, "Ede" } };

m.insert({ 1, "Willi" });

for (auto it = cbegin(m); it != cend(m); ++it)

{

cout << it->first << " -> " << it->second << '\n';

}

cout << endl;

for (auto x : m)

{

cout << x.first << " -> " << x.second << '\n';

}

}

Ausgabe

1 -> Willi

2 -> Heinrich

3 -> Ede

1 -> Willi

2 -> Heinrich

3 -> Ede

Beispiel

#include <iostream>

#include <map>

#include <string>

using namespace std;

int main()

{

map<string, string> m;

m.insert(map<string, string>::value_type("Detlef", "Wilkening"));

m.insert(map<string, string>::value_type("Bjarne", "Stroustrup"));

m.insert(map<string, string>::value_type("Dennis", "Ritchie"));

// Lesende Zugriffe

cout << "Wert von Detlef: " << m["Detlef"] << endl;

cout << "Wert von Bjarne: " << m["Bjarne"] << endl;

cout << "Wert von Dennis: " << m["Dennis"] << endl;

// Schreibender Zugriff

m["Detlef"] = "Wilky";

// Lesender Zugriff

cout << "Neuer Wert von Detlef: " << m["Detlef"] << endl;

}

Ausgabe

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 76 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Wert von Detlef: Wilkening

Wert von Bjarne: Stroustrup

Wert von Dennis: Ritchie

Neuer Wert von Detlef: Wilky

Beispiel:

#include <iostream>

#include <map>

#include <string>

using namespace std;

int main()

{

map<int, int> m1;

m1[0]; // Erzeugt einen Map-Eintrag "0 -> 0"

m1[1] = 0; // Erzeugt einen Map-Eintrag "1 -> 0"

m1[2] = 22; // Erzeugt einen Map-Eintrag "2 -> 22"

for (map<int, int>::const_iterator it = m1.begin(); it!=m1.end(); ++it)

{

cout << it->first << " -> " << it->second << endl;

}

cout << "Neu: " << m1[3] << '\n' << endl;

map<string, string> m2;

m2["leer"];

m2["empty"] = "";

m2["C++"] = "Programmiersprache";

for (map<string, string>::const_iterator it = m2.begin(); it!=m2.end(); ++it)

{

cout << it->first << " -> \"" << it->second << '"' << endl;

}

}

Ausgabe

0 -> 0

1 -> 0

2 -> 22

Neu: 0

C++ -> "Programmiersprache"

empty -> ""

leer -> ""

Suchen in der Map

#include <iostream>

#include <map>

#include <string>

using namespace std;

int main()

{

map<int, string> m;

m.insert(map<int, string>::value_type(2, "Heinrich"));

m.insert(map<int, string>::value_type(3, "Ede"));

m.insert(map<int, string>::value_type(1, "Ansgar"));

m.insert(map<int, string>::value_type(4, "Tom"));

map<int, string>::iterator fi = m.find(3);

if (fi!=m.end())

{

cout << "3 -> \"" << fi->second << '\n';

}

fi = m.find(7);

if (fi==m.end())

{

cout << "7 nicht gefunden\n";

}

}

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 77 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Ausgabe

3 -> "Ede

7 nicht gefunden

Header:

<map>

Beispiele von Konstruktoren:

map()

Erzeugt eine leere Map, d.h. eine Map mit null Elementen.

Beispiele von Element-Funktionen der Map:

bool empty() const

Gibt zurück, ob die Map leer ist.

size_type size() const

Gibt die Größe der Map zurück, d.h. die Anzahl an Elementen in der Map.

void insert(const value_type& val)

Fügt ein in „val“ enthaltenes Schlüssel/Wert-Paar als Kopie in die Map ein. Ist der

Schlüssel schon in der Map vorhanden – bezogen auf den Kleiner-Operator „<“ – so

passiert nichts.

T2& operator[ ]( const T1& t1)

Gibt den Wert zum Schlüssel „t1“ als Referenz zurück. Ist der Schlüssel „t1“ nicht in der

Map enthalten, so wird ein entsprechendes Schlüssel/Wert-Paar angelegt – der Wert mit

dem Standard-Konstruktor des Werts – und es wird eine Referenz auf den neu

angelegten Wert zurückgegeben.

void clear()

Löscht die Map.

size_type count(const T1& t1) const

Gibt die Anzahl an Schlüssel/Wert-Paaren mit dem Schlüssel „t1“ zurück – bei einer Map

ist dies immer „0“ oder „1“. Geht es nur um die Existenz eines Elements ohne Zugriff, so

ist diese Element-Funktion gegenüber der Element-Funktion „find“ vorzuziehen.

iterator find(const T1& t1) bzw. const_iterator find(const T1& t1) const

Sucht den Schlüssel „t1“ in der Map – mit dem Kleiner-Operator „<“. Wird der Schlüssel

gefunden, so wird ein Iterator auf das entsprechende Schlüssel/Wert-Paar

zurückgegeben. Ist der Schlüssel nicht im Set enthalten, so wird der End-Iterator

zurückgegeben.

iterator lower_bound(const T1& t1)

Liefert zu dem Paramter „t1“ einen Iterator auf ein exakt passendes Schlüssel/Wert Paar

– bezogen auf den Schlüssel, oder wenn dieses nicht existert, einen Iterator auf das

nächste Paar (dies kann auch der End-Iterator sein).

size_type erase(const T1& t1)

Löscht Schlüssel/Wert-Paar mit dem Schlüssel „t1“ aus der Map. Ist der Schlüssel „t1“

nicht in der Map enthalten, so passiert nichts. Die Funktion gibt zurück, wieviele

Schlüssel/Wert-Paare mit Schlüssel „t1“ gelöscht wurden – bei einer Map ist dies immer

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 78 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

„0“ oder „1“.

iterator erase(iterator it)

Löscht das Schlüssel/Wert-Paar, auf das der Lösch-Iterator „it“ zeigt, aus der Map. Die

Funktion gibt einen Iterator auf das Element nach dem Gelöschten zurück. Achtung – der

übergebene Lösch-Iterator ist nach dem Löschen nicht mehr gültig.

void erase(iterator first, iterator last)

Löscht die Schlüssel/Wert-Paare, die durch die Iteratoren „first“ (inkl) und „last“ (exkl.)

bestimmt werden, aus der Map. Die Funktion gibt nichts zurück.

void swap(map& m)

Vertauscht den Inhalt der Maps miteinander.

iterator begin() bzw. const_iterator begin() const

Gibt den (Const) Start-Iterator für die Map zurück.

iterator end() bzw. const_iterator end() const

Gibt den (Const) End-Iterator für die Map zurück.

iterator rbegin() bzw. const_iterator rbegin() const

Gibt den (Const) Start-Rückwärts-Iterator für die Map zurück.

iterator rend() bzw. const_iterator rend() const

Gibt den (Const) End-Rückwärts-Iterator für die Map zurück.

const_iterator cbegin() const

Gibt immer den Const-Start-Iterator für die Map zurück. (ISO C++11)

const_iterator cend() const

Gibt immer den Const-End-Iterator für die Map zurück. (ISO C++11)

const_iterator crbegin() const

Gibt immer den Const-Start-Rückwärts-Iterator für die Map zurück. (ISO C++11)

const_iterator crend() const

Gibt immer den Const-End-Rückwärts-Iterator für die Map zurück. (ISO C++11)

Beispiel:

#include <iostream>

#include <map>

using namespace std;

int main()

{

map<int, int> m;

cout << m.size() << " -> empty? " << m.empty() << '\n';

m.insert(map<int, int>::value_type(4, 11));

m.insert(map<int, int>::value_type(3, 22));

m.insert(map<int, int>::value_type(5, 33));

m.insert(map<int, int>::value_type(1, 44));

m.insert(map<int, int>::value_type(2, 55));

cout << m.size() << " -> empty? " << m.empty() << '\n';

for (map<int, int>::const_iterator it = m.begin(); it!=m.end(); ++it)

{

cout << it->first << "->" << it->second << ' ';

}

cout << '\n';

m.insert(map<int, int>::value_type(1, 4444));

m.insert(map<int, int>::value_type(2, 5555));

cout << m.size() << " -> empty? " << m.empty() << '\n';

for (map<int, int>::const_iterator it = m.begin(); it!=m.end(); ++it)

{

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 79 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

cout << it->first << "->" << it->second << ' ';

}

cout << '\n';

m[1] = 444;

m[6] = 666;

cout << m.size() << " -> empty? " << m.empty() << '\n';

for (map<int, int>::const_iterator it = m.begin(); it!=m.end(); ++it)

{

cout << it->first << "->" << it->second << ' ';

}

cout << '\n';

for (int i=5; i<8; ++i)

{

map<int, int>::const_iterator it = m.find(i);

if (it==m.end())

{

cout << "Element " << i << " ist nicht vorhanden.\n";

}

else

{

cout << "Element " << i << " ist vorhanden: " << it->second << '\n';

}

}

m.erase(2);

map<int, int>::iterator it = m.find(4);

m.erase(it);

cout << m.size() << " -> empty? " << m.empty() << '\n';

for (map<int, int>::const_iterator it = m.begin(); it!=m.end(); ++it)

{

cout << it->first << "->" << it->second << ' ';

}

cout << '\n';

m.clear();

cout << m.size() << " -> empty? " << m.empty() << '\n';

}

Ausgabe

0 -> empty? 1

5 -> empty? 0

1->44 2->55 3->22 4->11 5->33

5 -> empty? 0

1->44 2->55 3->22 4->11 5->33

6 -> empty? 0

1->444 2->55 3->22 4->11 5->33 6->666

Element 5 ist vorhanden: 33

Element 6 ist vorhanden: 666

Element 7 ist nicht vorhanden.

4 -> empty? 0

1->444 3->22 5->33 6->666

0 -> empty? 1

8.9 Weiteres zu den STL Containern

Alle STL Container halten ihre eigenen Kopien der zu speichernden Objekte

Alle Container in der STL sind typisiert – d.h. sie können nur Elemente eines bestimmten

Typs aufnehmen.

STL Container müssen intern natürlich Speicherplatz für die zu speichernden Objekte

anlegen.

Die STL beschreibt nicht die Implementierung der Container (also z.B. Sets als binärer

Baum), sondern nur ihr Verhalten.

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 80 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

9 Typ-System und mehr

9.1 Typ-Aliase

Typ-Aliase definieren neue Typ-Namen.

Achtung - keine neuen Typen, sondern nur neue Namen.

using length = int; // Definiert einen neuen Namen length fuer den Typ int

length len = 17; // Achtung - len ist weiter vom Typ int!

Wozu werden Typ-Aliase benutzt?

1. Um verständliche Typ-Namen bei komplexen oder unverständlichen Typen zu

bekommen.

2. Um einen konkreten Typ verstecken zu können.

3. Um semantische Typ-Namen zu erzeugen.

4. Um einen Typ leicht verändern zu können.

Verständliche Typ-Namen bei komplexen Typen

using container = map<int, string>;

using value = container::value_type;

using iter = container::const_iterator;

container m;

m.insert(value(2, "Heinrich"));

m.insert(value(3, "Ede"));

m.insert(value(1, "Ansgar"));

for (iter i=m.begin(); i!=m.end(); ++i)

{

cout << i->first << ' ' << i->second << '\n';

}

Ausgabe

1 Ansgar

2 Heinrich

3 Ede

Verstecken eines konkreten Typ’s

Siehe Typen wie „std::streamsize“ „std::string::size_type“.

Semantische Typ-Namen

void draw_rect(int, int, int, int);

Was bedeuten die int?

void draw_rect(int x1, int y1, int x2, int y2);

void draw_rect(int x, int y, int width, int height);

Aber welche Interpretation ist denn jetzt die richtige?

=> semantische Typ-Namen

using xcoor = int;

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 81 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

using ycoor = int;

void draw_rect(xcoor, ycoor, xcoor, ycoor);

Leichte Veränderung eines Typ’s

Vorher:

typedef int age;

Nachher:

typedef short age;

Hinweis: typedef

Man kann Typ-Aliase auch mit dem Schlüsselwort “typedef” erzeugen. “Using” hat aber eine

schönere Syntax und unterstützt z.B. Templates, weshalb sie “using” bevorzugen sollten.

9.2 Referenzen

Eine Referenz ist ein alternativer Name für ein Objekt.

Obwohl verschiedene Namen benutzt werden, ist immer das gleiche Objekt gemeint.

Referenz und Original-Objekt sind absolut äquivalent.

Eine Referenz wird definiert, in dem zwischen den Typ und den Variablennamen (hier auch

Referenznamen) das „Kaufmanns-Und“ „&“ eingefügt wird.

„Typ&“ bedeutet Referenz auf Typ

int i=17; // Integer-Variable i

int& r1=i; // Referenz r1 als zweiter Name zu i

int& r2=i; // " r2 als dritter " " i

cout << i << ' ' << r1 << ' ' << r2 << '\n'; // Ausgabe: 17 17 17

++i;

cout << i << ' ' << r1 << ' ' << r2 << '\n'; // Ausgabe: 18 18 18

r1=-4;

cout << i << ' ' << r1 << ' ' << r2 << '\n'; // Ausgabe: -4 -4 -4

r2+=32;

cout << i << ' ' << r1 << ' ' << r2 << '\n'; // Ausgabe: 28 28 28

Eine Referenz ist völlig gleichbedeutend zum Original-Objekt.

Eine Referenz verweist ihr gesamtes Leben auf das gleiche Objekt.

=>

Da eine Referenz als Synonym für ein Objekt steht, muss sie bei der Definition sofort

initialisiert werden.

double& rd; // Compiler-Fehler - eine Referenz muss initialisiert werden

Const-Referenzen

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 82 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

const int width = 65;

const int& rw = width;

++rw; // Compiler-Fehler, rw ist const

int sum = 89;

const int& rsum = sum;

++sum; // natuerlich okay

++rsum; // Compiler-Fehler, rsum ist const

const int ci = 17;

int& ri = ci; // Compiler-Fehler - const wuerde unterlaufen werden

Referenzen mit „auto“

#include <iostream>

using namespace std;

int main()

{

int n = 16;

auto& rn = n; // Erzeugt eine Int-Referenz, kein Int, da "auto" mit Referenz

cout << "Vor ++rn - n: " << n << endl; // -> Inital-Wert 16

++rn;

cout << "Nach ++rn - n: " << n << endl; // Ausgabe von 17, da "rn" Referenz ist

const auto& crn = n; // Erzeugt eine Const-Int-Referenz

cout << "Vor ++n - crn: " << n << endl; // -> Aktueller Wert 17

++n;

cout << "Nach ++n - crn: " << n << endl; // Ausgabe von 18, da "crn" Referenz ist

}

Ausgabe

Vor ++rn - n: 16

Nach ++rn - n: 17

Vor ++n - crn: 17

Nach ++n - crn: 18

Referenzen und Container

Aufgrund der Semantik von Referenzen beim Erzeugen (sie müssen initialisiert werden),

Kopieren und Zuweisen (es werden nicht die Referenzen kopiert bzw. zugewiesen, sondern

die referenzierten Objekte), können Container keine Referenzen aufnehmen.

vector<int&> v; // Compiler-Fehler – Referenzen in Containern sind nicht erlaubt

list<int&> l; // Compiler-Fehler – Referenzen in Containern sind nicht erlaubt

set<int&> s; // Compiler-Fehler – Referenzen in Containern sind nicht erlaubt

9.2.1 Referenzen vermeiden Kopien

#include <iostream>

#include <string>

#include <vector>

using namespace std;

int main()

{

vector<string> v;

v.push_back("Eins");

v.push_back("Zwei");

v.push_back("Drei");

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 83 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

for (string s : v) // Kopie

{

cout << s << ' ';

}

cout << endl;

}

Ausgabe

Eins Zwei Drei

#include <iostream>

#include <string>

#include <vector>

using namespace std;

int main()

{

vector<string> v;

v.push_back("Eins");

v.push_back("Zwei");

v.push_back("Drei");

for (string s : v)

{

s += "XXX"; // Aenderung ist nur in der lokalen "s" Variable

}

for (string s : v)

{

cout << s << ' '; // Ausgabe der initialen Werte im Container

}

cout << endl;

}

Ausgabe

Eins Zwei Drei

#include <iostream>

#include <string>

#include <vector>

using namespace std;

int main()

{

vector<string> v;

v.push_back("Eins");

v.push_back("Zwei");

v.push_back("Drei");

for (string& s : v) // Referenz!

{

s += "XXX"; // Aenderung bezieht sich jetzt auf den String im Container

}

for (string s : v) // (**)

{

cout << s << ' '; // Ausgabe der geaenderten Werte im Container

}

cout << endl;

}

Ausgabe

EinsXXX ZweiXXX DreiXXX

Die Referenz vermeidet Kopien =>

Meist bessere Performance und meist weniger Speicherbedarf)

Erlaubt uns hier zusätzlich die Modifikation der Original-Strings im Vektor.

#include <iostream>

#include <string>

#include <vector>

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 84 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

using namespace std;

int main()

{

vector<string> v;

v.push_back("Eins");

v.push_back("Zwei");

v.push_back("Drei");

for (string& s : v)

{

s += "XXX";

}

for (const string& s : v) // Const-Referenz wegen Performance und Speicher

{

cout << s << ' ';

}

cout << endl;

}

Ausgabe

EinsXXX ZweiXXX DreiXXX

#include <iostream>

#include <string>

#include <vector>

using namespace std;

int main()

{

vector<string> v;

v.push_back("Eins");

v.push_back("Zwei");

v.push_back("Drei");

for (vector<string>::iterator it=v.begin(); it!=v.end(); ++it)

{

string s = *it; // Kopie - Aenderungen nur lokal

s += "XXX";

}

for (vector<string>::const_iterator it=v.begin(); it!=v.end(); ++it)

{

cout << *it << ' ';

}

cout << endl;

}

Ausgabe

Eins Zwei Drei

#include <iostream>

#include <string>

#include <vector>

using namespace std;

int main()

{

vector<string> v;

v.push_back("Eins");

v.push_back("Zwei");

v.push_back("Drei");

for (vector<string>::iterator it=v.begin(); it!=v.end(); ++it)

{

string& s = *it; // Referenz – Aenderung im Container

s += "XXX";

}

for (vector<string>::const_iterator it=v.begin(); it!=v.end(); ++it)

{

cout << *it << ' ';

}

cout << endl;

}

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 85 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Ausgabe

EinsXXX ZweiXXX DreiXXX

9.2.2 Regel

Regel, wann was (Kopie, Referenz, Const-Referenz) verwendet werden sollte – siehe

Funktionen.

9.3 Aufzählungs-Typen

Typ, der nur bestimmte Werte annehmen kann.

Alignment-Werte: right, center, left, block

Farb-Werte: blue, red, green,...

Spielfarben-Werte: white, black

Spezielle Tasten-Werte: alt, ctrl, shift, meta, ...

enum class alignment { right, center, left, block };

enum class game_color { white, black };

enum class special_keys { shift, ctrl, alt, meta };

Ein Enum ist ein benutzerdefinierter Daten-Typ.

enum class alignment { right, center, left, block };

void fct(alignment); // Funktions-Deklaration mit Enum-Typ – bekommen wir noch

int main()

{

alignment a = alignment::center; // Okay – "a" Variable vom Typ "alginment"

a = alignment::block; // Okay – Zuweisungen gehen natuerlich auch

if (a == alignment::block) // Okay – Vergleiche sind moeglich

{

}

fct(a); // Okay

fct(alignment::left); // Okay

}

Die Enum-Werte sind Compile-Zeit-Konstanten.

Werden auch Enum-Konstanten oder Aufzählungs-Konstanten genannt.

Die Aufzählungs-Konstanten werden intern immer durch einen integralen Wert

dargestellt.

Wenn nicht anders definiert, werden für die Aufzählungs-Konstanten aufsteigende Werte

von ‚0’ an vergeben.

Enum-Variablen und Enum-Konstanten können explizit mit „static_cast“ in einen

integralen Wert gewandelt werden.

#include <iostream>

using namespace std;

int main()

{

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 86 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

enum class alignment { right, center, left, block };

cout << "right: " << static_cast<int>(alignment::right) << endl; // -> 0

cout << "center: " << static_cast<int>(alignment::center) << endl; // -> 1

cout << "left: " << static_cast<int>(alignment::left) << endl; // -> 2

cout << "block: " << static_cast<int>(alignment::block) << endl; // -> 3

alignment a = alignment::center;

cout << "a: " << static_cast<int>(a) << endl; // -> 1

int n = static_cast<int>(alignment::left);

cout << "n: " << n << endl; // -> 2

}

Ausgabe

right: 0

center: 1

left: 2

block: 3

a: 1

n: 2

Es können den Aufzählungs-Konstanten eigene Werte zugewiesen werden

#include <iostream>

using namespace std;

int main()

{

enum class align { right, center, left, block };

cout << "right: " << static_cast<int>(align::right) << endl; // -> 0

cout << "center: " << static_cast<int>(align::center) << endl; // -> 1

cout << "left: " << static_cast<int>(align::left) << endl; // -> 2

cout << "block: " << static_cast<int>(align::block) << endl; // -> 3

cout << endl;

enum class my_enum

{

val1 = 80, // val1 80

val2 = 90, // val2 90

val3, // val3 91

val4 = static_cast<int>(align::left), // val4 2

val5, // val5 3

val6 = 2, // val6 2

val7 // val7 3

};

cout << "val1: " << static_cast<int>(my_enum::val1) << endl; // -> 80

cout << "val2: " << static_cast<int>(my_enum::val2) << endl; // -> 90

cout << "val3: " << static_cast<int>(my_enum::val3) << endl; // -> 91

cout << "val4: " << static_cast<int>(my_enum::val4) << endl; // -> 2

cout << "val5: " << static_cast<int>(my_enum::val5) << endl; // -> 3

cout << "val6: " << static_cast<int>(my_enum::val6) << endl; // -> 2

cout << "val6: " << static_cast<int>(my_enum::val7) << endl; // -> 3

}

Ausgabe

right: 0

center: 1

left: 2

block: 3

val1: 80

val2: 90

val3: 91

val4: 2

val5: 3

val6: 2

val7: 3

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 87 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Häufig werden Enums in Verbindung mit einer Switch-Anweisung genutzt.

#include <iostream>

using namespace std;

enum class key { shift, ctrl, alt };

int main()

{

key k = key::shift;

switch (k)

{

case key::shift:

cout << "Taste: shift" << endl;

break;

case key::ctrl:

cout << "Taste: ctrl" << endl;

break;

case key::alt:

cout << "Taste: alt" << endl;

break;

default:

cout << "Fehler - unbekannte Taste" << endl;

}

}

Ausgabe

Taste: shift

9.4 Typ-Konvertierungen

9.4.1 Implizite Typ-Konvertierungen

double d = 3.14;

int i = d; // Implizite Typ-Konvertierung von "double" => "int"

float f = true; // Implizite Typ-Konvertierung von "bool" => "float"

char c = f; // Implizite Typ-Konvertierung von "float" => "char"

long l = 'A'; // Implizite Typ-Konvertierung von "char" => "long"

void fct(int); // Dies deklariert eine Funktion, die einen "int" erwartet

fct(3.14); // Ruft mit einem "double" die Funktion "fct(int)" auf

// Dabei wird das "double" Argument in einen "int" gecastet

// Implizite Typ-Konvertierung mit Daten-Verlust

9.4.2 Explizite Typ-Konvertierungen

Die aus C geerbte C-Konvertierung

Syntax: (ziel-typ)quell-ausdruck

Nicht empfohlen zu benutzen

Wird in der Vorlesung nicht detaillierter besprochen

Die Funktionale-Konvertierung, die prinzipiell gleichwertig zur C-Konvertierung ist.

Syntax: ziel-typ(quell-ausdruck)

Nicht empfohlen zu benutzen

Wird in der Vorlesung nicht detaillierter besprochen

Der Operator „static_cast“

Syntax: static_cast<ziel-typ>(quell-ausdruck)

Der Operator „const_cast“

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 88 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Syntax: const_cast<ziel-typ>(quell-ausdruck)

Wird in der Vorlesung nicht detaillierter besprochen

Der Operator „reinterpret_cast“

Syntax: reinterpret_cast<ziel-typ>(quell-ausdruck)

Wird in der Vorlesung nicht detaillierter besprochen

Der Operator „dynamic_cast“

Syntax: dynamic_cast<ziel-typ>(quell-ausdruck)

Wird im Thema Vererbung detaillierter besprochen

9.4.3 Konvertierungs-Operator „static_cast“

Der Operator „static_cast“ konvertiert u.a. folgende Typen in einander:

All die, die auch mit den normalen Sprachmitteln in einander umwandelbar sind

Integrale Werte können in enums und umgekehrt gewandelt werden

Syntax

static_cast<ziel-typ>(quell-ausdruck)

Beispiele

char c = static_cast<char>(65); // Okay, wuerde aber auch ohne "static_cast" gehen

int i = static_cast<int>(3.14); // Okay, wuerde aber auch ohne "static_cast" gehen

enum class color { red, blue, green };

color col = static_cast<color>(1); // Okay, benoetigt zwingend "static_cast"

9.5 Statische Assertions mit „static_assert“

Viele Programmierfehler treten auf, wenn bestehender Code verändert wird und die

Änderungen nicht nur an einer Stelle im Code, sondern an mehreren Stellen durchgeführt

werden müssen. Typische Fehler sind hierbei, dass nicht alle betroffenen Code-Stellen

angepaßt oder die Änderungen nicht konsistent durchgeführt werden.

Manchmal lassen sich verteilte Informationen nur minimieren aber nicht ganz vermeiden. In

diesem Fall sollte man versuchen, den Compiler zu bitten zu überprüfen ob die Code-Stellen

noch zueinander passen.

// Ein Enum fuer Farben

enum class color { white, black, red };

// Eine ganz andere Datei – weit weg von der Enum-Definition "color"

vector<string> color_names { "weiss", "schwarz", "rot" };

void print_color(color col)

{

// Achtung – dieser Code ist stark abhaengig von den Werten der Enum-Konstanten

cout << "Farbe: " << color_names[static_cast<int>(col)] << endl;

}

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 89 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Folgende Änderung führt zu einem Fehler im Code.

// Ein Enum fuer Farben

enum class color { blue, white, black, red }; // Neue Konstante – veraendert alle Werte

Sichern Sie solche Situationen mit „static_assert“ ab. Mit „static_assert“ schreiben Sie

Zusicherungen, die der Compiler überprüft. Wenn die Zusicherung erfüllt ist, passiert nichts

– wenn sie nicht erfüllt ist, dann gibt es einen Compiler-Fehler:

static_assert(1 == 1); // OK

static_assert(1 == 2, "Compiler-Error, da 1 nicht 2 ist"); // Compiler-Fehler

„static_assert“ bekommt 1 oder 2 Parameter übergeben:

Eine Compile-Zeit Bedingung, die sich zu „true“ oder „false“ auswerten läßt.

Einen optionalen Text als Compile-Fehler-Meldung, falls die Zusicherung „false“ ist

Mit einer entsprechenden statischen Assertion kann das obige Beispiel problemlos gegen

Änderungen gesichert werden:

vector<string> color_names { "weiss", "schwarz", "rot" };

static_assert(static_cast<int>(color::white) == 0, "Konstante color::white muss 0 sein");

static_assert(static_cast<int>(color::black) == 1, "Konstante color::black muss 1 sein");

static_assert(static_cast<int>(color::red) == 2, "Konstante color::red muss 2 sein");

void print_color(color col)

{

cout << "Farbe: " << color_names[static_cast<int>(col)] << endl;

}

10 Funktionen

10.1 Einführung

Funktionen bieten die Möglichkeit, Codefragmente zusammenzufassen und parametrisiert

von beliebiger Stelle aus zunutzen.

void f()

{

cout << "Hallo Welt\n";

}

int main()

{

f();

f();

}

Ausgabe

Hallo Welt

Hallo Welt

10.1.1 Funktions-Deklaration

Damit der Compiler in ISO C++ einen Funktionsaufruf compiliert, muss die Funktion

deklariert werden – man nennt die Deklaration auch Bekanntmachung oder Prototyp. Durch

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 90 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

die Deklaration wird dem Compiler die Funktion bekannt gemacht

Syntax (Funktions-Deklaration):

Rückgabetyp Funktionsname(Parameterliste);

// Beispiele fuer Funktions-Deklarationen

void f1();

int f2(int);

double f3(string s, char);

Die Deklaration besteht aus:

Rückgabetyp – dies muss ein für den Compiler bekannter Typ sein, der im Normalfall

kopierbar sein muss.

Funktionsname – ein im Rahmen der C++ Namensregeln erlaubter und in seinem Kontext

eindeutiger Name – über diesen Namen wird auf die Funktion später zugegriffen.

Runde Klammer auf

Parameterliste

Die Parameterliste darf leer sein.

Ansonsten ist sie eine durch Komma getrennte Liste von Parametern.

Ein Parameter ist ein Typ und ein optionaler Name

Runde Klammer zu

Abschliessendes Semikolon

int main()

{

fct(); // Compiler-Fehler, da die Funktion fct nicht bekannt ist

}

void fct(); // Funktions-Deklaration

int main()

{

fct(); // Compiliert, da die Funktion bekannt ist, und alles stimmt.

fct(1); // Compiler-Fehler, da die Argumente nicht zur Funktion passen.

}

10.1.2 Funktions-Definition

Zusätzlich muss die Funktion natürlich noch irgendwo implementiert werden. Eine solche

Implementierung wird Funktions-Definition oder auch Funktions-Implementierung genannt.

Syntax (Funktions-Definition)

Rückgabetyp Funktionsname(Parameterliste) { Funktionscode }

// Beispiel fuer Funktions-Definitionen

void f1()

{

cout << "Hallo Welt\n";

}

int f2(int x)

{

int erg = 2*x-1;

if (erg<0)

{

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 91 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

erg = 99;

}

return erg;

}

void f3(string s, char)

{

}

Hinweis – eine Funktions-Definition enthält auch immer implizit ihre Funktions-Deklaration.

void f()

{

}

// void f(); // Deklaration nicht notwendig

int main()

{

f(); // Okay, da die Definition implizit die Deklaration enthaelt

}

10.2 Parameter und Argumente

In den meisten Fällen bekommen Funktionen Argumente übergeben.

void print(int n)

{

cout << n << ' ';

}

int main()

{

for (int i=0; i<5; ++i)

{

print(i);

}

}

Ausgabe

0 1 2 3 4

Namen:

Auf der Seite der Funktion: Parameter – d.h. „n“ ist ein Parameter

Auf der Seite des Funktions-Aufrufs: Argument – d.h. „i“ ist ein Argument

Parameter sind im Prinzip normale lokale Variablen der Funktion, nur das sie von außen an

die Funktion beim Aufruf mitgegeben werden. Ansonsten gilt – wie für lokale Variablen:

Sie sind nur innerhalb der Funktion sichtbar (zugreifbar).

Mit Verlassen des Scopes (hier der Funktion) werden sie automatisch zerstört!

Sie sind lokal zu diesem Funktionsaufruf, d.h. ein zweiter paralleller Funktionsaufruf

enthält seinen eigenen Parameter (bzw. erzeugt seine eigene lokale Variable), die

vollkommen unabhängig sind. Weiter unten findet sich dazu noch ein Beispiel.

Parameter werden an Funktionen entweder im sogenannten „call-by-value“ (cbv) oder

„call-by-reference“ (cbr) Verfahren übergeben.

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 92 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

10.2.1 call-by-value

Im Normallfall werden Argumente in C++ per Wert übergeben. Das heißt, dass das an die

Funktion eine Kopie des Arguments übergeben wird. Veränderungen des Parameters (d.h.

der lokalen Kopie) betreffen das Argument (d.h. die Original-Variable) nicht!

void f(int arg)

{

cout << "f1: " << arg << '\n';

++arg;

cout << "f2: " << arg << '\n';

}

int main()

{

int n = 4;

cout << "m1: " << n << '\n';

f(n);

cout << "m2: " << n << '\n'; // n ist unveraendert 4

}

Ausgabe

m1: 4

f1: 4

f2: 5

m2: 4

10.2.2 call-by-reference

Als Alternative zu „cbv“ gibt es „cbr“, bei dem keine Kopie des Arguments erzeugt wird,

sondern statt dessen eine Referenz (ein Verweis) auf das Argument der Funktion übergeben

wird. Alle Veränderungen betreffen immer das von der Referenz referenzierte Objekt!

void f(int& arg) // Hier steht jetzt zusaetzlich ein &

{

cout << "f1: " << arg << '\n'; // <- hier wird ueberall ueber die

++arg; // <- Referenz 'arg' die Variable 'n'

cout << "f2: " << arg << '\n'; // <- in main angesprochen.

}

int main()

{

int n = 4;

cout << "m1: " << n << '\n';

f(n);

cout << "m2: " << n << '\n'; // n ist 5

}

Ausgabe

m1: 4

f1: 4

f2: 5

m2: 5

10.2.3 Wann was wie warum – und const-Referenzen

Im ersten Augenblick scheint die Sache klar zu sein:

Soll die Original-Variable nicht verändert werden, nimm call-by-value.

Soll die Original-Variable verändert werden, nimm call-by-reference.

Aber in C++ ist fast nichts so einfach, wie es zuerst ausschaut. Hier gilt noch zu bedenken,

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 93 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

dass es in C++ Typen gibt:

die nicht kopierbar sind (z.B. Streams), und

bei denen eine Kopie teuer ist (speicher- und performance-mäßig, z.B. ein Vektor).

void f1(std::ostream); // nicht kopierbar

void f2(std::vector<std::string>); // Kopie ist teuer

In beiden Fällen empfiehlt sich call-by-reference, auch wenn die Original-Variable nicht

verändert werden soll. Damit fängt man sich aber zwei Probleme ein:

Die Original-Variable könnte unbeabsichtigt doch verändert werden.

Die Funktion ist für konstante Objekte nicht mehr nutzbar.

void f1(int arg)

{

++arg; // Unbeabichtige Aenderung macht kein Problem

}

void f2(int& arg)

{

++arg; // Unbeabichtige Aenderung IST EIN PROBLEM

}

void g(int&);

int main()

{

const int i = 9;

g(i); // Compiler-Fehler, da i ueber g veraendert werden koennte!!

}

Beide Probleme können mit dem Modifier „const“ gelöst werden. Wird der Referenz-

Parameter „const“ gemacht, so kann der Compiler Problem 1 finden, und Problem 2 gibt es

nicht mehr.

void f(const int& arg)

{

++arg; // Compiler-Fehler

}

void g(const int&);

int main()

{

const int i = 9;

g(i); // Alles okay, da i ueber g nicht veraendert werden kann

}

Nun sind in der Praxis zwar erstaunlich viele Typen nicht kopierbar, aber sie sind trotzdem

die Ausnahme. Viel wichtiger ist in der Praxis das Argument „Ressourcenverbrauch“ und

hierbei vor allem das Thema Performance. Schon bei relativ einfachen und kleinen Objekten

können die Performance-Kosten eines Kopiervorgangs höher sein als die Kosten durch

indirekte Objektzugriffe.

10.2.4 Parameter-Übergabe-Regel

Für uns führt das zu der “nicht-vollständigen“ Parameter-Übergabe-Regel:

Sollen Original-Objekte in der Funktion verändert werden => call-by-reference

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 94 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

- Es geht nicht anders – mit den anderen Aufruf-Arten können wir das Original-Objekt nicht

ändern.

Elementare Datentypen, Enums, Zeiger, usw... => call-by-value

- Alle diese Typen (auch Basis-Daten-Typen) können von der Maschine sehr effizient gehandelt

werden – daher ist die Übergabe per “cbv” effizient und unproblematisch.

Alle anderen => call-by-const-reference

- Alle anderen Typen können meist nicht effizient kopiert werden bzw. sind vielleicht gar nicht

kopierbar – daher bietet sich die Referenz-Übergabe an. Da das Original-Objekt aber nicht

geändert werden soll, nehmen wir natürlich Const-Referenzen.

10.3 Rückgaben

Eine Funktion kann – aber muss nicht – ein Ergebnis zurückgeben – dies wird mit dem

Rückgabe-Typ festgelegt.

Soll eine Funktion nichts zurückgeben, so wird der Pseudotyp „void“ als Rückgabe-Typ in

der Deklaration und Definition benutzt.

Hat eine Funktion einen Ergebnis-Rückgabe-Typ (d.h. nicht „void“), so muss die Funktion

mit einer „return“ Anweisung enden.

Syntax: return ausdruck;

int f()

{

return 2*4+8;

}

int f()

{

} // Compiler-Fehler – es wird am Funktions-Ende keine Ergebnis zurueck gegeben.

Mit einer „return“ Anweisung wird die Funktion instantan beendet.

Der Ausdruck in der „return“ Anweisung wird berechnet.

Alle erzeugten lokalen Variablen und dieParameter werden in der umgekehrten

Reihenfolge ihrer Konstruktion zerstört.

Das Ausdrucks-Ergebnis wird als Kopie zurückgegeben (ausser die Funktion hat einen

Referenz-Typ als Rückgabe-Typ).

Eine Funktion kann beliebig viele Ausgänge haben, d.h. es können in einer Funktion beliebig

viele return-Anweisungen stehen

int f(int arg)

{

if (arg<0)

{

return -1;

}

if (arg==0)

{

return 0;

}

return 1;

}

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 95 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Auch Funktionen ohne Rückgabe – d.h. mit „void“ als Rückgabe-Typ – können jederzeit

beendet werden. In diesem Fall reicht eine return Anweisung mit leerem Ausdruck.

void f(int arg)

{

if (arg<0)

{

return;

}

...

}

10.3.1 Referenzen

Auch Referenzen können als Rückgabetyp eingesetzt werden.

int& fct(int& arg)

{

arg *= 2;

return arg;

}

int main()

{

int i=9;

cout << fct(i); // -> 18

}

Das Beispiel sieht vielleicht etwas komisch aus, da der Parameter (in veränderter Form)

zurückgegeben wird. Dies war aber notwendig, da es bei der Rückgabe von Referenzen

eine Falle gibt: es dürfen nämlich niemals Referenzen auf lokale Variablen zurückgegeben

werden.

int& f()

{

int i = 42;

return i; // Achtung – gefaehrliches Laufzeitproblem - niemals machen!

}

int main()

{

cout << f(); // Worauf verweist die Referenz hier?

}

Bedenken Sie, dass eine Referenz ein Verweis auf ein Objekt ist, in diesem Fall ist die

Rückgabe daher der Verweis auf die lokale Variable „i“. Aber beim Verlassen der Funktion

werden lokale Variablen automatisch zerstört, d.h. sie existieren nicht mehr. Die Referenz

verweist also auf ein Objekt, das nicht mehr existiert. Der Zugriff erzeugt undefiniertes

Verhalten.

10.3.2 Nicht zwingende Nutzung

Wenn eine Funktion einen Wert zurückgibt, so kann dieser ignoriert werden.

int f()

{

return 2;

}

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 96 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

int main()

{

f(); // Rueckgabe wird nicht benutzt – das ist okay

}

10.3.3 Const Rückgaben

Wollen Sie auch eigentlich-änderbare Rückgaben unveränderlich machen, können sie

natürlich auch Rückgabe-Typen mit dem Modifier „const“ schützen.

#include <iostream>

#include <string>

using namespace std;

const string f() // Das const ist neu{

return "C++";

}

int main()

{

cout << f().insert(1, "DE") << '\n'; // Compiler-Fehler

}

10.3.4 Neue C++11/C++14 Syntax für Funktions-Rückgaben

In C++11 wurde eine neue Syntax für Funktions-Rückgaben eingeführt.

#include <iostream>

using namespace std;

auto f() -> void // Neue C++11 Syntax

{

cout << "void f()" << endl;

}

auto g(int arg) -> int // Neue C++11 Syntax

{

cout << "int g(" << arg << ')' << endl;

return 2*arg;

}

auto main() -> int // Auch fuer "main" geht die neue C++11 Syntax

{

f();

int n = g(3);

cout << "n: " << n << endl;

}

Ausgabe

void f()

int g(3)

n: 6

Für normale Funktionen sind die alte und neue Syntaxen absolut identisch.

In C++14 wurde die Syntax noch weiter vereinfacht. Wenn dem Compiler die Funktions-

Definition bekannt ist und alle Return-Anweisungen den gleichen Typ zurückgeben, dann

kann der Compiler den Rückgabe-Typ ja selber deduzieren.

#include <iostream>

using namespace std;

auto f() // Neue C++14 Syntax – ohne -> Typ

{

return 2;

}

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 97 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

auto g(bool arg) // Neue C++14 Syntax – ohne -> Typ

{

if (arg)

{

return 2.7;

}

return 3.14;

}

int main()

{

cout << boolalpha;

cout << "f(): " << f() << endl;

cout << "g(true): " << g(true) << endl;

cout << "g(false): " << g(false) << endl;

}

Ausgabe

f(): 2

g(true): 2.7

g(false): 3.14

10.3.5 Rückgabe neuer Objekte

Man kann das Rückgabe-Objekt direkt in der Return-Anweisung konstruieren, indem man

die funktionale Konvertierung anwendet „typ(initialwerte)“. Genau genommen wird hier durch

die Konvertierung ein temporäres Objekt erzeugt.

string createNewString(const string& in)

{

if (in.empty())

{

return string(); // <= Objekt direkt beim "return" erzeugen

}

string::size_type len = in.length();

char first = in[0];

return string(len, first); // <= Objekt direkt beim "return" erzeugen

}

In C++11 kann man hier auch eine implizite Konvertierung mit den geschweiften Klammern

nutzen:

string createNewString(const string& in)

{

if (in.empty())

{

return {}; // <= geschweifte Klammern erzeugen hier leeren String

}

string::size_type len = in.length();

char first = in[0];

return {len, first}; // <= geschweifte Klammern erzeugen den String auch so

}

10.4 Konvertierungen

Wie schon erwähnt, kann es auch bei Funktions-Aufrufen und Funktions-Rückgaben zu

impliziten Konvertierungen kommen.

Wenn eine Funktion nicht die genau passenden Parameter-Typen erwartet, und die Sprache

entsprechende Konvertierungen erlaubt – dann konvertiert der Compiler die Argumente

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 98 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

implizit.

#include <iostream>

using namespace std;

void f(int n)

{

cout << "f(" << n << ')' << endl;

}

void g(double d)

{

cout << "g(" << d << ')' << endl;

}

int main()

{

f(2); // Aufruf von "f" mit passendem "int"

f(3.14); // Aufruf von "f" mit "double" => konvertiert zu "int"

g(8); // Aufruf von "g" mit "int" => konvertiert zu "double"

g(9.23); // Aufruf von "g" mit passendem "double"

}

Ausgabe

f(2)

f(3)

g(8)

g(9.23)

Zeichenketten-Konstanten sind keine Strings sind. Trotzdem kann man Funktionen

schreiben, die Strings erwarten und sich mit Zeichenketten-Konstanten aufrufen lassen –

implizite Typ-Konvertierung sei Dank.

#include <iostream>

#include <string>

using namespace std;

void f(const string& s)

{

cout << "f(" << s << ')' << endl;

}

int main()

{

string str("std::string");

f(str); // Aufruf von "f" mit "string"

f("Kein std::string"); // Aufruf von "f" ohne "string" => Typ-Konvertierung

}

Ausgabe

f(std::string)

f(Kein std::string)

Diese impliziten Typ-Umwandlungen stehen natürlich nicht nur für die Funktions-Argumente,

sondern auch für die Funktions-Rückgabe zur Verfügung.

#include <iostream>

using namespace std;

double h()

{

return 2.7;

}

int main()

{

double d = h(); // Rueckgabe ist "double" – alles okay

cout << "d: " << d << endl;

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 99 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

int n = h(); // Rueckgabe ist "double", kein "int" => Konvertierung

cout << "n: " << n << endl;

}

Ausgabe

d: 2.7

n: 2

Der Compiler darf pro impliziter Konvertierungen immer beliebig viele von der Sprache

definierte Typ-Umwandlungen, und max. eine benutzerdefinierte Typ-Umwandlung

vornehmen. Benutzerdefinierte Typ-Umwandlungen sind entweder Konvertierungs-

Konstruktoren oder Konvertierungs-Operatoren.

Natürlich ist man nicht auf die implizite Typ-Konvertierung angewiesen. Wenn man möchte –

weil sie z.B. nicht gut lesbar oder verwirrend ist – dann kann man sie auch explizit machen.

#include <iostream>

#include <string>

using namespace std;

void f(int n)

{

cout << "f(" << n << ')' << endl;

}

void g(const string& s)

{

cout << "g(" << s << ')' << endl;

}

int main()

{

f(static_cast<int>(3.14)); // Explizite Konvertierung mit "static_cast"

f(string("Literal")); // Explizite Konvertierung im funktionalen C++ Stil

}

Ausgabe

f(3)

g(Literal)

10.5 Default-Argumente

In der Praxis wäre es oft angenehm, bei einem Funktionsaufruf nicht alle Parameter

angeben zu müssen

In C++ können nämlich Default-Argumente vorgegeben werden, die automatisch vom

Compiler bei einem Aufruf ohne diese Parameter benutzt werden. Dazu werden die Default-

Argumente einfach in der Funktions-Parameterliste den Parametern zugewiesen.

void f(int=1);

f(); // => f(1)

f(4); // => f(4)

Es können beliebig viele Default-Argumente benutzt werden - sie müssen aber ohne

Lücke immer die letzten Parameter der Signatur abdecken, da sonst Mehrdeutigkeiten

auftreten könnten.

Default-Argumente dürfen auch in Verbindung mit Parameternamen auftauchen.

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 100 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

void f(int i=4, char c='A', long l=17);

f(); // => f(4, 'A', 17)

f(0); // => f(0, 'A', 17)

f(2, 'B'); // => f(2, 'B', 17)

f(5, 'M', 12); // => f(5, 'M', 12)

// Funktionen mit fehlerhaften Default-Argumenten.

// Die Default-Argumente decken die letzten Parameter nicht buendig ab

void f1(int=4, char, long=17); // Compiler-Fehler

void f2(int, int=9, int); // Compiler-Fehler

10.6 Überladen

In C++ gehört die Parameterliste einer Funktion (die sogenannte Signatur) zum Funktions-

Namen, d. h. unterschiedliche Funktionen können den gleichen Namen haben, solange sie

sich durch ihre Signatur unterscheiden. Dies nennt man Überladen oder vollständiger

„Funktions-Überladung“.

int f();

int f(bool);

int f(int);

int f(char);

int f(signed char);

int f(unsigned char);

int f(long, int);

int f(string&, int);

int f(string&);

int f(const string&);

Der Rückgabetyp zählt nicht zur Signatur bzw. zum Funktions-Namen und trägt daher nicht

zur Unterscheidung bei. Man kann Funktionen also nicht nur mit dem Rückgabe-Typ

überladen.

Beim Aufruf einer Funktion entscheidet der Compiler anhand der Argumente, welche

Funktion er nimmt.

#include <iostream>

using namespace std;

void f(int) // Funktion f mit int

{

cout << "int\n";

}

void f(char) // Funktion f mit char

{

cout << "char\n";

}

void f(int, char) // Funktion f mit int und char

{

cout << "int, char\n";

}

int main()

{

f(4); // Ausgabe: int

f('C'); // " char

f(2, 'A'); // " int, char

}

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 101 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Typ-Aliase sind keine neuen Typen, sondern nur Alias-Namen für bestehende Typen. Daher

können Funktionen natürlich nicht mit solchen Typ-Aliasen überladen werden.

using length = int;

void f(int);

void f(length); // Erneute Deklaration der gleichen Funktion -> erlaubt

void f(int)

{

}

void f(length) // Compiler-Fehler - erneute Definition der Funktion "void f(int)"

{

}

10.6.1 Konvertierungs-Hierarchien und Mehrdeutigkeiten

Leider gibt es im Detail dann doch ein Problem mit Überladen: was ist wenn es keine exakt

passende Funktion gibt, aber mehrere prinzipiell mögliche Funktionen?

void f(short);

void f(long);

int main()

{

f(4); // "4" ist ein "int"

} // -> Welche Funktion wird aufgerufen? "short" oder "long" oder was passiert?

// Hier Compiler-Fehler

Diese Frage läßt sich nicht so einfach allgemein beantworten – in diesem konkreten Beispiel

(„int=>long“ oder „int=>short“) bekommt man einen Compiler-Fehler, da hier der Funktions-

Aufruf für den Compiler mehrdeutig ist. Die Sprache erlaubt implizite Konvertierungen vom

„int“ zum „long“ und auch zum „short“. Beide Konvertierungen sind integrale

Konvertierungen, und aus Sicht der Sprache absolut gleichwertig – darum erzeugt dies

einen Compiler-Fehler.

Nun gilt dies aber nicht für alle Wandlungen – sie sind nicht alle gleichwertig. Alle

Konvertierungen in C++ sind Teil einer Konvertierungs-Hierarchie. Die genauen Regeln

sprengen leider bei weitem den Umfang dieses Skripts. Hier eine teilweise grobe Übersicht:

1. Exakte Übereinstimmung

- D.h. keine Konvertierung notwendig

2. Triviale Wandlungen ohne semantische Änderungen

- Typ => Typ&

- Const Typ => const Typ&

3. Triviale Wandlungen mit semantische Änderungen

- Typ => const Typ&

4. Integrale & Fließkomma Konvertierungen

- int => long

- int => short

- long long => char

- float => double

5. Elementare Konvertierungen

- int => double

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 102 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Ist eine Konvertierung in dieser Hierarchie eher vorhanden, so greift sie – ohne dass es zu

einer Mehrdeutigkeit kommt. Konvertierungen auf der gleichen Hierarchie-Ebene führen zu

Mehrdeutigkeiten und damit zu Compiler-Fehlern.

Aus dieser Hierarchie ergibt sich u.a., daß sich z.B. eine Referenz und eine Const-Referenz

überladen lassen. Ein Non-Const Objekt bindet dabei eher an die normale Referenz (Stufe

2) als die Const-Referenz (Stufe 3), während ein Const-Objekt nur an die Const-Referenz

binden kann, was hier Stufe 2 entspricht:

#include <iostream>

using namespace std;

void f(string& s) // Funktion f mit String Referenz

{

cout << "Non-Const String Referenz\n";

}

void f(const string& s) // Funktion f mit Const String Referenz

{

cout << "Const String Referenz\n";

}

int main()

{

string s("Non-Const");

const string cs("Const");

f(s); // Nimmt f(string&), da non-const Objekte eher an non-const Referenzen binden

f(cs); // Nimmt f(const string&), da const Objekte nur an const Referenzen binden

}

Ausgabe

Non-Const String Referenz

Const String Referenz

10.7 Parameter und lokale Variablen

Parameter, die keine Referenzen sind, und lokale Variablen sind in C++ wirklich

funktionslokal, d.h. jeder Funktionsaufruf bekommt seinen eigenen Satz an Parametern und

lokalen Variablen. Hier ein etwas sinnloses Beispiel. Beachten Sie, dass die Variablen „arg“

und „loc“ lokal zu jedem Funktionsaufruf sind.

void f(int arg)

{

int loc = arg + 1;

cout << "f(" << arg << ")\n";

cout << "- loc: " << loc << '\n';

arg += 10;

loc += 20;

cout << "- arg: " << arg << '\n';

cout << "- loc: " << loc << '\n';

if (arg==10)

{

cout << " arg==10 => return\n";

return;

}

f(0);

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 103 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

cout << "- arg: " << arg << '\n';

cout << "- loc: " << loc << '\n';

cout << " return\n";

}

int main()

{

f(5);

}

Ausgabe

f(5)

- loc: 6

- arg: 15

- loc: 26

f(0)

- loc: 1

- arg: 10

- loc: 21

arg==10 => return

- arg: 15

- loc: 26

return

Hier kann man sehr schön sehen, dass der erneute Aufruf der Funktion „f“ seine eigenen

Parameter und lokalen Variablen bekommt, und deren Veränderung keine Veränderung an

den Parametern und lokalen Variablen der ersten Aufrufs vornimmt. Diesen Effekt – das

eine Funktion zur gleichen Zeit mehrfach aufgerufen wird – bekommt man z.B. bei

Rekursionen oder Multi-Threading.

10.8 Rekursion

Wenn eine Funktion im gleichen Thread mehrfach ineinander (d.h. nicht nacheinander,

sondern gleichzeitig parallel) aufgerufen wird, nennt man das „Rekursion“ bzw. „rekursive

Programmierung“.

Eine Funktion muss sich dabei nicht zwangsläufig selber aufrufen, sondern dies kann auch

über mehrere Zwischenstationen passieren, z.B. f -> g -> h -> i -> j -> f. Solche Fälle sind

häufig gar nicht mehr sofort zu erkennen, von daher passiert dies in der Praxis häufiger, als

man vielleicht im ersten Augenblick denkt.

Es gibt aber auch viele Probleme, die sich rekursiv viel viel leichter programmieren lassen

als ohne Rekursion. Hier ein Beispiel, das man in der Praxis sicher nicht rekursiv lösen

würde – die Summe aller Zahlen von 1 bis n.

int sum(int arg)

{

if (arg<=1) // Operator <= statt == um die Funktion bei fehlerhaften

{ // Argumenten (arg<1) sauber zu beenden.

return 1;

}

int erg = arg + sum(arg-1);

return erg;

}

int main()

{

cout << sum(5) << '\n';

}

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 104 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Ausgabe

15

Hierbei wird quasi direkt die mathematische Definition umgesetzt:

sum(1) := 1

sum(n) := n + sum(n-1)

Natürlich läßt sich die Summe der Zahlen von 1 bis n direkt über die Formel „n*(n+1)/2“

berechnen – was hier auch viel sinnvoller wäre – aber es geht eben auch rekursiv.

Es läßt sich beweisen, dass jedes Problem was iterativ gelöst werden kann (d.h. mit einer

Schleife) sich auch rekursiv lösen läßt, und umgekehrt. In den aller-meisten Fällen sind die

iterativen Lösungen schneller und benötigen weniger Speicher – sie sind daher vorzuziehen.

Ein Schleifen-Durchlauf ist einfach schnell und effizient, während ein Funktions-Aufruf doch

relativ teuer ist (bezogen auf die Performance). In manchen Fällen ist die rekursive Lösung

aber ein 5-Zeiler, während die iterative Lösung harte Arbeit sein kann und hinterher aus

vielen Zeilen schwer lesbarem Quelltext besteht.

10.9 Inline-Funktionen

Funktionen können „geinlined“ werden. Dann versucht der Compiler statt des

Funktionsaufrufs, die Funktions-Implementierung selber an die Stelle des Aufrufs zu setzen.

Dies führt zu einer Performance-Verbesserung, da der Overhead des Funktionsaufrufs

wegfällt. Dazu muss vor die Funktion das Schlüsselwort „inline“ gesetzt werden.

inline int min(int a, int b)

{

return a<b ? a : b;

}

int main()

{

int v = 12;

for (int i=10; i<15; ++i)

{

cout << min(i, v) << ' '; // Hier wird die Funktions-Implementierung expandiert

} // Es findet kein Funktions-Aufruf statt

cout << '\n';

}

Ausgabe

10 11 12 12 12

Hinweise:

„inline“ ist nur ein Bitte an den Compiler, kein Befehl

„inline“ funktioniert nur, wenn der Compiler an der Stelle des Funktions-Aufrufs die

Funktions-Implementierung kennt.

Der Compiler darf auch ohne das Schlüsselwort „inline“ inlinen. Das Schlüsselwort

verhindert eigentlich nur Linker-Fehler

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 105 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

10.10 Funktions-Templates

todo

10.11 Constexpr-Funktionen

Todo

11 Standard-Bibliothek

11.1 File-Streams

Analog zu den Streams für Konsole/Tastatur gibt es auch File-Streams für Dateien.

Headerdatei: <fstream>

File-Ausgabe-Klasse: std::ofstream

File-Eingabe-Klasse: std::ifstream

Konstruktion mit dem Datei-Namen (optional mit absolutem oder relativem Pfad).

#include <iostream>

#include <fstream>

int main()

{

// Der Scope um die folgenden zwei Zeilen sorgt fuer

// das automatische Schliessen des Ausgabestroms – s.u.

{

std::ofstream out("temp.txt"); // Ausgabestrom oeffnen und

out << 3.14 << 'A' << 14; // Werte rausschieben

} // Eigentlich mit Fehlerbehandlung – s.u.

int i;

char c;

double d;

std::ifstream in("temp.txt"); // Eingabestrom oeffnen und

in >> d >> c >> i; // Werte einlesen

// Eigentlich mit Fehlerbehandlung – s.u.

std::cout << "double: " << d

<< "\nchar: " << c

<< "\nint: " << i

<< '\n';

}

Ausgabe

double: 3.14

char: A

int: 14

Achtung – das Schreiben in und das Lesen aus Dateien ist natürlich im höchsten Maße

kritisch, d.h. kann immer schief gehen. Hier sollten Sie niemals auf eine adäquate

Fehlerbehandlung verzichten.

Datei-Ende und Fehler – Status EOF und FAIL

Der Status FAIL bedeutet, dass nichts gelesen werden konnte – aus welchen Gründen

auch immer.

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 106 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Umgekehrt heißt das, wenn der Stream nicht im Status FAIL ist, dass das Einlesen

geklappt hat, und die Eingabe ausgewertet werden kann.

Beispiel

#include <iostream>

#include <fstream>

using namespace std;

int main()

{

{

ofstream out("temp.txt");

out << "abcd";

}

ifstream in("temp.txt");

for (;;)

{

char c;

in >> c;

if (in.fail()) break;

cout << "-> " << c << '\n';

}

}

Ausgabe

-> a

-> b

-> c

-> d

Beispiel

#include <iostream>

#include <fstream>

using namespace std;

int main()

{

{

ofstream out("temp.txt");

out << "abcd";

}

ifstream in("temp.txt");

for (;;)

{

char c;

in >> c;

if (in.fail()) break;

cout << "-> " << c << '\n';

}

cout << (in.eof() ? "EOF" : "Fehler") << '\n';

}

Ausgabe

-> a

-> b

-> c

-> d

EOF

Beispiel

#include <iostream>

#include <fstream>

using namespace std;

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 107 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

int main()

{

{

ofstream out("temp.txt");

out << "23 45 98";

}

ifstream in("temp.txt");

for (;;)

{

int i;

in >> i;

if (in.fail()) break;

cout << "-> " << i << '\n';

}

cout << (in.eof() ? "EOF" : "Fehler") << '\n';

}

Ausgabe

-> 23

-> 45

-> 98

EOF

Beispiel – Zeilenweises Einlesen

#include <string>

#include <iostream>

#include <fstream>

using namespace std;

int main()

{

{

ofstream out("temp.txt");

out << "Zeile 1\nZeile 2\nZeile 3";

}

ifstream in("temp.txt");

for (string s;;)

{

getline(in, s);

if (in.fail()) break;

cout << "-> " << s << '\n';

}

cout << (in.eof() ? "EOF" : "Fehler") << '\n';

}

Ausgabe

-> Zeile 1

-> Zeile 2

-> Zeile 3

EOF

Beispiel – Fehler

#include <iostream>

#include <fstream>

using namespace std;

int main()

{

{

ofstream out("temp.txt");

out << "23 45 xx 98"; // << hier falsche Zeichen fuer Integer

}

ifstream in("temp.txt");

for (;;)

{

int i;

in >> i;

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 108 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

if (in.fail()) break;

cout << "-> " << i << '\n';

}

cout << (in.eof() ? "EOF" : "Fehler") << '\n';

}

Ausgabe

-> 23

-> 45

Fehler

Ein typischer Fehler ist die Abfrage auf EOF statt auf FAIL.

EOF sagt nur, dass beim Lesen das Datei-Ende erreicht wurde.

Mit EOF wird nicht ausgesagt, dass nichts mehr eingelesen wurde.

Nur FAIL gibt an, ob was gelesen werden konnte, oder nicht.

EOF gibt nur an, ob zusätzlich noch das Datei-Ende erreicht wurde.

// Achtung - fehlerhafter Code! Auch wenn es scheinbar funktioniert!!!

#include <string>

#include <iostream>

#include <fstream>

using namespace std;

int main()

{

{

ofstream out("temp.txt");

out << "Zeile 1\nZeile 2\nZeile 3\n"; // mit '\n' am Ende

}

string s;

ifstream in("temp.txt");

for (;;)

{

getline(in, s);

if (in.eof()) break; // So nicht, niemals

cout << '"' << s << "\"\n";

}

}

Ausgabe

"Zeile 1"

"Zeile 2"

"Zeile 3"

Leider funktioniert das Beispiel schon nicht mehr, wenn die Datei nicht mit einem ‚\n’

abgeschlossen ist.

// Achtung - fehlerhafter Code! Auch wenn es scheinbar funktioniert!!!

#include <string>

#include <iostream>

#include <fstream>

using namespace std;

int main()

{

{

ofstream out("temp.txt");

out << "Zeile 1\nZeile 2\nZeile 3"; // ohne '\n' am Ende

}

string s;

ifstream in("temp.txt");

for (;;)

{

getline(in, s);

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 109 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

if (in.eof()) break; // so nicht, niemals

cout << '"' << s << "\"\n";

}

}

Ausgabe

"Zeile 1"

"Zeile 2"

Machen Sie es richtig:

Erst versuchen zu lesen, dann den Stream-Status auswerten.

Hier primär FAIL auswerten, und dann sekundär noch EOF.

Und nur Eingaben auswerten, wenn der Stream-Status GOOD ist.

Datei zum Anhängen öffnen

#include <iostream>

#include <fstream>

using namespace std;

int main()

{

{

ofstream out("temp.txt");

out << "12";

}

{

ofstream out("temp.txt"); // Loescht den alten Inhalt "12"

out << "34";

}

{

int n;

ifstream in("temp.txt");

in >> n;

cout << n << endl; // => nur "34"

}

{

ofstream out("temp.txt", ios::app); // Oeffnet zum Anhaengen dank Flag "app"

out << "56";

}

{

int n;

ifstream in("temp.txt");

in >> n;

cout << n << endl; // => "3456"

}

}

Ausgabe

34

3456

11.2 Filesystem-Library

Typische Aufgaben:

Existiert eine bestimmte Datei oder ein bestimmtes Verzeichnis?

Ist ein Name eine Datei oder ein Verzeichnis?

Wie groß ist eine Datei?

Welche Dateien befinden sich in einem Verzeichnis?

Kopieren, verschieben oder löschen von Dateien und/oder Verzeichnissen.

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 110 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Treten bei der Nutzung der Filesystem-Library Laufzeit-Probleme auf, so wird die

Filesystem-Library dies mit einer Exceptions melden. Nun lernen wir Exceptions leider in der

Vorlesung nicht kennen. Sie sollten also nur Programme schreiben, die immer funktionieren

;-)

Test-Daten

Diese Struktur wird bei allen Beispielen und Aufgaben benutzt.

Test-Verzeichnis-Struktur

C:\

└ DateiSystemBeispiele

└ verzeichnis1

│ └ verzeichnis3

│ │ └ verzeichnis4

│ │ │ └ find.txt - 2286 Byte

│ │ │ └ search.dat - 1680 Byte

│ │ │ └ test.txt - 270 Byte

│ │ └ test.txt - 42 Byte

│ └ verzeichnis5

│ │ └ find.txt - 683 Byte

│ │ └ test.txt - 482 Byte

│ └ daten.dat - 192 Byte

│ └ test.txt - 1298 Byte

└ verzeichnis2

│ └ find.txt - 250 Byte

│ └ test.txt - 3078 Byte

└ search.dat - 1600 Byte

└ test.txt - 912 Byte

Existenz, Typ und Größe für Dateien ermitteln

bool exists(const boost::filesystem::path&);

Gibt zurück, ob das übergebene Element (Datei, Verzeichnis,…) existiert

bool is_regular_file(const boost::filesystem::path&);

Gibt zurück, ob das übergebene Element eine normale Datei ist

Achtung – existiert das Element nicht, so wird eine Exception geworfen

bool is_directory(const boost::filesystem::path&);

Gibt zurück, ob das übergebene Element ein Verzeichnis ist

Achtung – existiert das Element nicht, so wird eine Exception geworfen

boost::uintmax_t file_size(const boost::filesystem::path&);

Gibt die Größe des übergebenen Elements zurück

Achtung – existiert die Datei nicht oder ist das Element keine Datei, so wird eine

Exception geworfen

// Achtung – in dieser Form ist das Programm nur unter Windows lauffaehig

// Unter anderen Systemen muessen die Pfad-Namen angepasst werden.

#include <iostream>

#include <string>

#include <vector>

#include <filesystem>

using namespace std;

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 111 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

// Funktion gibt aus, ob das uebergebene Element existiert

void check_file_existence(const string& file)

{

cout << "- " << file << " => " << exists(file) << endl;

}

// Funktion gibt den Typ des uebergebenen Elements aus, wenn es existiert

void check_file_type(const string& file)

{

cout << "- " << file << " => ";

if (!exists(file))

{

cout << "---\n";

return;

}

if (is_regular_file(file))

{

cout << "Datei\n";

}

else if (is_directory(file))

{

cout << "Verzeichnis\n";

}

else

{

cout << "unbekannter Typ\n";

}

}

// Funktion gibt die Groesse des uebergebenen Elements aus,

// wenn es existiert und eine Datei ist

void check_file_size(const string& file)

{

cout << "- " << file << " => ";

if (exists(file) && is_regular_file(file))

{

cout << file_size(file) << " Byte\n";

}

else

{

cout << "---\n";

}

}

int main()

{

cout << boolalpha;

vector<string> files;

// Korrekter Datei-Name

files.push_back("C:\\DateiSystemBeispiele\\search.dat");

// Fehlerhafter Datei-Name

files.push_back("C:\\DateiSystemBeispiele\\xy.z");

// Korrekter Verzeichnis-Name

files.push_back("C:\\DateiSystemBeispiele");

// Fehlerhafter Verzeichnis-Name

files.push_back("C:\\DateiSystemBeispiele\\kein-pfad\\search.dat");

cout << "Datei-Existenz:" << endl;

for (vector<string>::const_iterator it=files.begin(); it!=files.end(); ++it)

{

check_file_existence(*it);

}

cout << endl;

cout << "Datei-Typ:" << endl;

for (vector<string>::const_iterator it=files.begin(); it!=files.end(); ++it)

{

check_file_type(*it);

}

cout << endl;

cout << "Datei-Groesse:" << endl;

for (vector<string>::const_iterator it=files.begin(); it!=files.end(); ++it)

{

check_file_size(*it);

}

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 112 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

}

Ausgabe (unter Voraussetzung der obigen Datei-Struktur)

Datei-Existenz:

- C:\DateiSystemBeispiele\search.dat => true

- C:\DateiSystemBeispiele\xy.z => false

- C:\DateiSystemBeispiele => true

- C:\DateiSystemBeispiele\kein-pfad\search.dat => false

Datei-Typ:

- C:\DateiSystemBeispiele\search.dat => Datei

- C:\DateiSystemBeispiele\xy.z => ---

- C:\DateiSystemBeispiele => Verzeichnis

- C:\DateiSystemBeispiele\kein-pfad\search.dat => ---

Datei-Groesse:

- C:\DateiSystemBeispiele\search.dat => 1600 Byte

- C:\DateiSystemBeispiele\xy.z => ---

- C:\DateiSystemBeispiele => ---

- C:\DateiSystemBeispiele\kein-pfad\search.dat => ---

Filesystem.Path:

Die Path-Klasse kapselt einen plattform-unabhängigen Datei-Namen (inkl. Pfad).

Außerdem unterstützt die Path-Klasse die verschiedenen Zeichen-Codes Ihrer jeweiligen

Plattform.

Zusätzlich hat die Path-Klasse viele hilfreiche Element-Funktionen – zum Beispiel:

“string” native() const;

Gibt den Pfad im nativen Format zurück.

Achtung – die Rückgabe muß nicht vom Typ “std::string” sein, den wir kennen.

“string” string() const;

Gibt den Pfad als String zurück.

Achtung – die Rückgabe muß nicht vom Typ “std::string” sein, den wir kennen.

“string” generic_string() const;

Gibt den Pfad als generischen quasi plattform-unabhängigen String zurück.

Achtung – die Rückgabe muß nicht vom Typ “std::string” sein, den wir kennen.

bool has_root_name() const;

Gibt zurück, ob das Path-Objekt einen Root-Namen enthält.

Ein Root-Name ist z.B. eine Netzwerk-Location oder ein Laufwerk-Identifier

path root_name() const;

Gibt den Root-Namen des Path-Objekts als Path-Objekt zurück.

Existiert kein Root-Name, so wird ein leeres Path-Objekt zurückgegeben.

bool has_root_directory() const;

Gibt zurück, ob das Path-Objekt ein Root-Verzeichnis enthält.

path root_directory() const;

Gibt das Root-Verzeichnis des Path-Objekts als Path-Objekt zurück.

Existiert kein Root-Verzeichnis, so wird ein leeres Path-Objekt zurückgegeben.

bool has_root_path() const;

Gibt zurück, ob das Path-Objekt einen Root-Pfad enthält.

path root_path() const;

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 113 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Gibt den Root-Pfad des Path-Objekts als Path-Objekt zurück.

Existiert kein Root-Pfad, so wird ein leeres Path-Objekt zurückgegeben.

bool has_filename() const;

Gibt zurück, ob das Path-Objekt einen Datei-Namen enthält.

path filename() const;

Gibt den Datei-Namen des Path-Objekts als Path-Objekt zurück.

Existiert kein Datei-Name, so wird ein leeres Path-Objekt zurückgegeben.

bool has_stem() const;

Gibt zurück, ob das Path-Objekt einen Haupt-Datei-Namen (Stamm) enthält.

path stem() const;

Gibt den Haupt-Datei-Namen (Stamm) des Path-Objekts als Path-Objekt zurück.

Existiert kein Haupt-Datei-Name, so wird ein leeres Path-Objekt zurückgegeben.

bool has_extension() const;

Gibt zurück, ob das Path-Objekt eine Extension enthält.

path extension() const;

Gibt die Extension des Path-Objekts als Path-Objekt zurück.

Existiert keine Extension, so wird ein leeres Path-Objekt zurückgegeben.

Und ein zugehöriges Beispiel-Programm – basierend auf der Datei-Struktur.

// Achtung – in dieser Form ist das Programm nur unter Windows lauffaehig

// Unter anderen Systemen muss der Pfad-Name angepasst werden.

#include <iostream>

#include <filesystem>

using namespace std;

int main()

{

cout << boolalpha;

path p("C:\\DateiSystemBeispiele\\verzeichnis1\\daten.dat");

cout << "Exist: " << exists(p) << '\n';

cout << "Direc: " << is_directory(p) << '\n';

cout << "File: " << is_regular_file(p) << '\n';

cout << "Size: " << file_size(p) << '\n';

// "Neue" Element-Funktionen der Klasse "path"

cout << "Nativ: " << p << '\n';

cout << "Strin: " << p.string() << '\n';

cout << "Gener: " << p.generic_string() << '\n';

cout << "Ro-Na: " << p.has_root_name() << " - " << p.root_name() << '\n';

cout << "Ro-Di: " << p.has_root_directory() << " - " << p.root_directory() << '\n';

cout << "Ro-Pa: " << p.has_root_path() << " - " << p.root_path() << '\n';

cout << "Filen: " << p.has_filename() << " - " << p.filename() << '\n';

cout << "Stem: " << p.has_stem() << " - " << p.stem() << '\n';

cout << "Exten: " << p.has_extension() << " - " << p.extension() << '\n';

}

Ausgabe (unter Voraussetzung der obigen Datei-Struktur)

Exist: true

Direc: false

File: true

Size: 192

Nativ: "C:\DateiSystemBeispiele\verzeichnis1\daten.dat"

Strin: C:\DateiSystemBeispiele\verzeichnis1\daten.dat

Gener: C:/DateiSystemBeispiele/verzeichnis1/daten.dat

Ro-Na: true - "C:"

Ro-Di: true - "\"

Ro-Pa: true - "C:\"

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 114 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Filen: true - "daten.dat"

Stem: true - "daten"

Exten: true - ".dat"

Verzeichnis auslesen

// Achtung – in dieser Form ist das Programm nur unter Windows lauffaehig

// Unter anderen Systemen muss der Pfad-Name angepasst werden.

#include <iostream>

#include <filesystem>

using namespace std;

int main()

{

cout << boolalpha;

directory_iterator it("C:\\DateiSystemBeispiele\\verzeichnis1\\verzeichnis3");

directory_iterator eit;

for (; it!=eit; ++it)

{

const path& p = it->path();

cout << "Native: " << p << '\n';

cout << "- Name: " << p.filename() << '\n';

cout << "- Datei: " << is_regular_file(p) << '\n';

cout << "- Verzei: " << is_directory(p) << '\n';

if (is_regular_file(p))

{

cout << "- Groesse: " << file_size(p) << '\n';

}

cout << endl;

}

}

Ausgabe (unter Voraussetzung der obigen Datei-Struktur)

Native: "C:\DateiSystemBeispiele\verzeichnis1\verzeichnis3\test.txt"

- Name: "test.txt"

- Datei: true

- Verzei: false

- Groesse: 42

Native: "C:\DateiSystemBeispiele\verzeichnis1\verzeichnis3\verzeichnis4"

- Name: "verzeichnis4"

- Datei: false

- Verzei: true

Rekursive Datei-Suche – selber implementiert

// Achtung – in dieser Form ist das Programm nur unter Windows lauffaehig

// Unter anderen Systemen muss der Pfad-Name angepasst werden.

#include <iostream>

#include <string>

#include <filesystem>

using namespace std;

void searchFileInPath(const path& searchpath, const string& searchfile)

{

if (!is_directory(searchpath))

{

cout << "Fehler - " << searchpath << " ist kein Verzeichnis" << endl;

return;

}

directory_iterator it(searchpath);

directory_iterator eit;

for (; it!=eit; ++it)

{

const path& p = it->path();

if (is_regular_file(p) && p.filename()==searchfile)

{

cout << "-> " << p << '\n';

}

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 115 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

else if (is_directory(p))

{

searchFileInPath(p, searchfile);

}

}

}

int main()

{

cout << "Rekursive Datei-Suche mit Std::Filesystem" << endl;

const string searchfile("find.txt");

path searchpath("C:\\DateiSystemBeispiele");

cout << "Suche in: " << searchpath << endl;

cout << "Nach: " << searchfile << endl;

searchFileInPath(searchpath, searchfile);

}

Mögliche Ausgabe (Andere Reihenfolge moeglich – die ist nicht definiert)

Rekursive Datei-Suche mit Boost.Filesystem

Suche in: "C:\DateiSystemBeispiele"

Nach: find.txt

-> "C:\DateiSystemBeispiele\verzeichnis1\verzeichnis3\verzeichnis4\find.txt"

-> "C:\DateiSystemBeispiele\verzeichnis1\verzeichnis5\find.txt"

-> "C:\DateiSystemBeispiele\verzeichnis2\find.txt"

Rekursive Datei-Suche – Rekursiver Iterator

// Achtung – in dieser Form ist das Programm nur unter Windows lauffaehig

// Unter anderen Systemen muss der Pfad-Name angepasst werden.

#include <iostream>

#include <string>

#include <filesystem>

using namespace std;

void searchFileInPath(const path& searchpath, const string& searchfile)

{

if (!is_directory(searchpath))

{

cout << "Fehler - " << searchpath << " ist kein Verzeichnis" << endl;

return;

}

recursive_directory_iterator it(searchpath);

recursive_directory_iterator eit;

for (; it!=eit; ++it)

{

const path& p = it->path();

if (is_regular_file(p) && p.filename()==searchfile)

{

cout << "-> " << p << '\n';

}

// Behandlung von Verzeichnissen geloescht

}

}

int main()

{

cout << "Rekursive Datei-Suche mit Std::Filesystem" << endl;

const string searchfile("find.txt");

path searchpath("C:\\DateiSystemBeispiele");

cout << "Suche in: " << searchpath << endl;

cout << "Nach: " << searchfile << endl;

searchFileInPath(searchpath, searchfile);

}

Mögliche Ausgabe (Andere Reihenfolge moeglich – die ist nicht definiert)

Rekursive Datei-Suche mit Boost.Filesystem

Suche in: "C:\DateiSystemBeispiele"

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 116 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Nach: find.txt

-> "C:\DateiSystemBeispiele\verzeichnis1\verzeichnis3\verzeichnis4\find.txt"

-> "C:\DateiSystemBeispiele\verzeichnis1\verzeichnis5\find.txt"

-> "C:\DateiSystemBeispiele\verzeichnis2\find.txt"

11.3 Exit

Man kann ein Programm jederzeit hart beenden, in dem man die Funktion „std::exit(int)“ aus

dem Header „cstdlib“ aufruft.

#include <iostream>

#include <cstdlib>

using namespace std;

void fct()

{

cout << "fct start\n";

exit(1);

cout << "fct ende\n";

}

int main()

{

cout << "main start\n";

fct();

cout << "main ende\n";

}

Ausgabe

main start

fct start

11.4 Zufallszahlen

#include <iostream>

#include <random>

using namespace std;

int main()

{

mt19937 engine;

uniform_int_distribution<> dist(-4, +6);

for (int i=0; i<10; ++i)

{

cout << dist(engine) << ' ';

}

cout << endl;

}

Mögliche Ausgabe

2 -1 6 5 0 3 -4 2 1 0

Wenn Sie dieses Programm mehrfach starten, so wird das Programm bei jedem Aufruf

immer die gleichen Zahlen ausgibt – dies ist sicher nicht sehr zufällig. Man muss für zufällige

Zahlen einen Start-Wert („seed“) angeben. Die Lösung heute ist, sich einen

„std::random_device“ Generator zu erzeugen, nur einen Wert auszulesen und diesen als

Seed für seinen eigentlichen Generator zu verwenden.

#include <iostream>

#include <random>

using namespace std;

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 117 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

int main()

{

random_device rd; // Random-Device-Generator erzeugen

mt19937 engine(rd()); // Seed Argument auslesen und nutzen

uniform_int_distribution<> dist(-4, +6);

for (int i=0; i<10; ++i)

{

cout << dist(engine) << ' ';

}

cout << endl;

}

Mögliche Ausgabe

5 5 -3 4 -1 1 -4 -3 -1 -4

11.5 Mathematische Funktionen

Viele mathematische Funktionen finden sich im Header <cmath>

Wurzeln mit „std::sqrt“

#include <cmath>

#include <iostream>

using namespace std;

int main()

{

cout << "sqrt( 1. ): " << sqrt( 1.) << '\n';

cout << "sqrt( 4.0): " << sqrt( 4.0) << '\n';

cout << "sqrt( 9.0): " << sqrt( 9.0) << '\n';

cout << "sqrt( 10.0): " << sqrt( 10.0) << '\n';

cout << "sqrt( 64.0): " << sqrt( 64.0) << '\n';

cout << "sqrt(259.21): " << sqrt(259.21) << '\n';

}

Ausgabe

sqrt( 1. ): 1

sqrt( 4.0): 2

sqrt( 9.0): 3

sqrt( 10.0): 3.16228

sqrt( 64.0): 8

sqrt(259.21): 16.1

Zehner-Logarithmus mit „std::log10“

#include <cmath>

#include <iostream>

using namespace std;

int main()

{

cout << "log10( 1. ): " << log10( 1.) << '\n';

cout << "log10( 1.5): " << log10( 1.5) << '\n';

cout << "log10( 9.9): " << log10( 9.9) << '\n';

cout << "log10( 10.0): " << log10( 10.0) << '\n';

cout << "log10( 12.3): " << log10( 12.3) << '\n';

cout << "log10( 98.7): " << log10( 98.7) << '\n';

cout << "log10(123.4): " << log10(123.4) << '\n';

cout << "log10(999.9): " << log10(999.9) << '\n';

}

Ausgabe

log10( 1. ): 0

log10( 1.5): 0.176091

log10( 9.9): 0.995635

log10( 10.0): 1

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 118 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

log10( 12.3): 1.08991

log10( 98.7): 1.99432

log10(123.4): 2.09132

log10(999.9): 2.99996

11.6 Pairs und Tuple

Beim Programmieren haben wir häufiger das Problem, mal schnell zwei, drei oder mehr

Elemente (Variablen) gemeinsam handeln zu müssen. Im Normallfall sollten wir dafür

Klassen verwenden. Aber manchmal sind Klassen aufwändiger als notwendig, da man

einfach nur ganz lokal und kurz z.B. 2 Variablen gemeinsam handeln möchte. Für diese

einfachen Situationen gibt es in C++ Pairs und Tuple.

Pairs

#include <iostream>

#include <string>

#include <utility>

using namespace std;

int main()

{

pair<int, string> pa;

pa.first = 4;

pa.second = "Pairs sind manchmal sehr hilfreich";

cout << pa.first << endl;

cout << pa.second << endl;

}

Ausgabe

4

Pairs sind manchmal sehr hilfreich

Beispiel

#include <iostream>

#include <utility>

using namespace std;

int main()

{

pair<int, double> pa0; // <-- kein Argument

cout << pa0.first << " - " << pa0.second << endl;

pair<int, double> pa2(8, 3.14); // <-- zwei Argumente

cout << pa2.first << " - " << pa2.second << endl;

}

Ausgabe

0 - 0

8 - 3.14

Beispiel

#include <iostream>

#include <utility>

using namespace std;

int main()

{

auto pa1 = make_pair(1, 2);

cout << "=> " << pa1.first << " - " << pa1.second << endl;

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 119 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

auto pa2 = make_pair(true, 'A');

cout << "=> " << pa2.first << " - " << pa2.second << endl;

auto pa3 = make_pair(123L, 3.14);

cout << "=> " << pa3.first << " - " << pa3.second << endl;

}

Ausgabe

=> 1 - 2

=> 1 - A

=> 123 - 3.14

Tuple

Pairs sind auf exakt 2 Werte beschränkt.

Tuple funktionieren für 0..n Werte.

#include <iostream>

#include <tuple>

using namespace std;

int main()

{

// Tuple verschiedener Groesse

tuple<> tu0; // Groesse 0 - hat nur akademischen Wert

tuple<int> tu1; // Groesse 1 - wie eine einzelne Int-Variable

tuple<int, int> tu2; // Groesse 2 - koennte man auch pair<> nehmen

tuple<int, int, int> tu3; // Groesse 3 - jetzt wird es interessant

tuple<int, int, int, int> tu4; // usw.

tuple<int, int, int, int, int> tu5; // ...

// Konstruktion von Tuplen

tuple<int, int, int> t0;

tuple<int, int, int> t3(1, 2, 3);

// Zugriff auf die Attribute von Tuplen nur mit "std::get<>()"

cout << get<0>(t0) << " - " << get<1>(t0) << " - " << get<2>(t0) << '\n';

cout << get<0>(t3) << " - " << get<1>(t3) << " - " << get<2>(t3) << '\n';

cout << endl;

// Die Funktion "std::make_tuple" mit "auto" macht das Leben wiedermal leichter

auto t = make_tuple(11, 22L, 33.3);

cout << get<0>(t) << " - " << get<1>(t) << " - " << get<2>(t) << '\n';

}

Ausgabe

0 - 0 - 0

1 - 2 - 3

11 – 22 - 33.3

12 Klassen

12.1 Motivation

In der Praxis benötigt man häufig mehrere Variablen, die logisch zusammenhängen, um zu

beschreiben, was man darstellen möchte.

Beispiele:

Bruch

– int Zähler

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 120 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

– int Nenner

Complexe Zahl

– double Realteil

– double Imaginärteil

Datum

– int Jahr

– int Monat

– int Tag

Person

– string Vorname

– string Nachname

– string Straße

– string Ort

– vector<string> Telefonnummern

Außerdem möchte man:

Die Interna der Klassen sollen von außen nicht zugreifbar sein.

Objekte sollen immer initialisiert sein.

Objekte sollen immer sauber zerstört werden.

Objekte sollen immer sauber kopiert und gewiesen werden.

Objekte sollen die Funktionen, die auf sie arbeiten, enthalten.

Darum hat man in der Objektorientierung das Sprachmittel von Klassen eingefügt, das

neben der Adressierung dieser Themen auch noch viele weitere Möglichkeiten enthält, z.B.

Vererbung und Polymorphie.

12.2 Klassen-Definition

Eine Klasse ist in C++ ein benutzerdefinierter Typ, und daher gilt für sie alles, was wir

für Typen kennengelernt haben.

Eine Klasse muss in C++ immer definiert werden, bevor ihre Elemente (Klassen-Variablen,

Element-Funktionen, Konstruktoren,...) implementiert werden können, oder die Klasse

benutzt werden kann.

class date

{

public:

void init(int, int, int);

void print();

private:

int day_;

int month_;

int year_;

};

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 121 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

12.2.1 Attribute

Die Attribute eine Klasse sind ganz normale Variablen, die einfach zu einem neuen Typ

zusammengefasst werden.

12.2.2 Element-Funktionen

Element-Funktionen sind im Prinzip ganz normale Funktionen. Es gelten daher alle

bekannten Features z.B. bzgl. Deklaration, Überladen, Default-Argumente, Parameterliste,

usw. Und im Beispiel sehen die Element-Funktions-Deklarationen ja auch wie ganz normale

Deklarationen aus - und sind es auch.

12.2.3 Zugriffsspezifizierer

Mit den Schlüsselwörtern public und private werden Zugriffsrechte vergeben. Auf alle

Elemente (Attribute, Funktionen,...)

im public-Bereich kann von ausserhalb und innerhalb der Klasse,

im private-Bereich kann nur von innerhalb der Klasse

zu gegriffen werden.

12.2.4 Element-Funktions-Definitionen

Die Element-Funktionen einer Klasse müssen - als normale Funktionen - natürlich definiert

werden. Der offensichtlichste Unterschied gegenüber der Definition von freien Funktionen ist

ein aufwändigerer Funktions-Name, da sich dieser aus Klassen-Name, Scope-Resolution-

Operator und dem eigentlichen Funktions-Namen zusammensetzt.

Syntax:

Rückgabetyp Klassen-Name :: Funktionsname ( Parameterliste ) { Anweisungen }

// Achtung - die Definition der Klasse 'date' muss dem Compiler bekannt sein!

// Daher die Klassen-Definition muss vorher im Quelltext stehen.

void date::init(int d, int m, int y)

{

day_ = d;

month_ = m;

year_ = y;

}

void date::print()

{

std::cout << day_ << '.' << month_ << '.' << year_;

}

Element-Funktionen sind fest an eine Klasse und ihren Kontext gebunden. Von daher muss

die Klasse definiert worden sein, bevor eine Element-Funktion definiert werden kann, damit

der Kontext (der Aufbau der Klasse) bekannt ist.

Element-Funktion einer Klasse können auf die private Attribute der Klasse zugegreifen, da

sie Teil der Klasse sind und damit auch Zugriff auf den private-Bereiche haben.

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 122 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

12.2.5 Klassen-Benutzung

Mit der Definition von „date“ ist „date“ zu einem benutzerdefinierten Datentyp geworden.

Daher können von ihm Objekte angelegt werden - Syntax: „typ variablenname;“.

// Achtung - die Definition der Klasse 'date' muss dem Compiler bekannt sein!

// Achtung - die Element-Funktions Definitionen muessen fuer den Linker vorhanden sein!

int main()

{

date d1; // ein Datums-Objekt wird erzeugt

date d2, d3; // zwei Datums-Objekte werden erzeugt

d1.init(29, 11, 2004); // d1 wird mit dem 29.11.2004 initialisiert

d2.init(10, 5, 1999); // d2 wird mit dem 10.05.1999 initialisiert

d3.init( 5, 1, 2005); // d3 wird mit dem 05.01.2005 initialisiert

d1.print(); // => 29.11.2004

d2.print(); // => 10.05.1999

d3.print(); // => 05.01.2005

}

Auf die Objekt-Komponenten kann über das Objekt mit dem Punkt-Operator zugegriffen

werden - hier im Bsp. sieht man dies für die Element-Funktionen „init“ und „print“. Die

Element-Funktionen greifen hierbei auf die Daten des Objekts zu, mit dem sie aufgerufen

wurden, d. h. auf die Daten des aktuellen Objekts. Element-Funktionen haben immer

einen Objektbezug.

12.3 Zugriffsbereiche

Der Default-Bereich in der Klassen-Definiton ist private.

Ein Zugriffsbereich darf leer sein.

Die Zugriffsbereiche dürfen mehrfach in beliebiger Reihenfolge vorkommen.

class A

{

void f1();

long l1;

public:

void f2();

long l2;

private:

void f3();

long l3;

private:

void f4();

long l4;

private:

public:

void f5();

long l5;

};

int main()

{

A a;

a.f1(); // Compiler-Fehler -> kein Zugriff von aussen, da private

a.l1=7; // " -> " " " " , " "

a.f2(); // okay, Zugriff von aussen erlaubt, da public

a.l2=7; // " , " " " " , " "

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 123 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

a.f3(); // Compiler-Fehler -> kein Zugriff von aussen, da private

a.l3=7; // " -> " " " " , " "

a.f4(); // Compiler-Fehler -> kein Zugriff von aussen, da private

a.l4=7; // " -> " " " " , " "

a.f5(); // okay, Zugriff von aussen erlaubt, da public

a.l5=7; // " , " " " " , " "

}

Der public-Bereich wird oft als Interface bezeichnet, da er die Schnittstelle der Klasse

nach aussen darstellt.

Alle Attribute sollten private sein. Zugriff auf die Attribute nur über Element-

Funktionen. Im Sinne einer sauberen und sicheren Programmierung sollten alle Attribute

in den private-Bereich einer Klasse liegen und alle Zugriffe über Element-Funktionen

abgewickelt werden.

12.4 Klassen sind benutzerdefinierte Typen

Klassen sind benutzerdefinierte Typen, und verhalten sich wie wir von Typen erwarten:

1. Referenzen (auch const) auf Objekte sind möglich.

2. Kopieren und Zuweisen ist möglich

3. Aufruf an Funktionen mit cbv (default) und cbr möglich.

void f_cbv(date d) // call-by-value, d.h. Kopie wird angelegt

{

d.print(); // => 29.11.2004

d.init(24, 12, 2005);

d.print(); // => 24.12.2005

}

void f_cbr(date& d) // call-by-reference

{

d.print(); // => 29.11.2004

d.init(24, 12, 2005);

d.print(); // => 24.12.2005

}

int main()

{

date d, d2;

d.init(29, 11, 2004);

d.print(); // => 29.11.2004

f_cbv(d);

d.print(); // => 29.11.2004

f_cbr(d);

d.print(); // => 24.12.2005

d2 = d;

d2.print(); // => 24.12.2005

}

12.5 Erweiterung

Wunsch – ein Datums Objekt soll mit dem aktuellen Datum initialisiert werden können.

Lösung – z.B. zweite Element-Funktion „init“ ohne Parameter (d.h. Überladen).

#include <ctime>

class date

{

public:

void init(); // Neue Funktion – Rest wie bisher

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 124 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

void init(int, int, int);

void print();

private:

int day_;

int month_;

int year_;

};

// Initialisiert das date-Objekt mit dem aktuellem Datum.

void date::init()

{

std::time_t timer = std::time(0);

std::tm* tblock = std::localtime(&timer);

day_ = tblock->tm_mday;

month_ = tblock->tm_mon+1;

year_ = tblock->tm_year+1900;

}

date d;

d.init();

d.print(); // Ausgabe aktuelles Datum

Hinweis – man sollte in der Realität die Element-Funktion vielleicht aussagekräftiger

„set_to_now“ oder „today“ oder so nennen. Aber ich wollte dies auch gleich als Beispiel

nutzen, dass sich natürlich auch Element-Funktionen überladen lassen.

12.6 Objekt-Zustand

Problem – es kann passieren, dass ein Datums-Objekt ein Datum repräsentiert, das es

nicht gibt, z. B. den 789.-2.0

Lösung – um dies zu verhindern, bauen wir eine private Testfunktion ein, die nach jeder

Änderung des inneren Zustands aufgerufen wird, und diesen auf Korrektheit überprüft. Im

Falle eines Problems wird eine Fehlermeldung ausgegeben und das Programm hart mit der

Funktion „std::exit(int)“ aus „cstdlib“ beendet.

#include <iostream>

#include <cstdlib>

using namespace std;

class date

{

public:

void init();

void init(int, int, int);

void print();

private:

void test(int, int, int); // Neue Funktion – Rest wie bisher

int day_;

int month_;

int year_;

};

void date::init(int d, int m, int y)

{

test(d, m, y); // Und spaeter auch an allen anderen relevanten Stellen

day_ = d;

month_ = m;

year_ = y;

}

// Testet, ob das Datums Objekt okay ist, und beendet im Fehlerfall mit

// einer Meldung das Programm - die Implementierung ist dem Praktikum

// ueberlassen.

void date::test(int d, int m, int y)

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 125 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

{

if ( ??? )

{

cout << "Datums-Objekt ";

print();

cout << " ist nicht korrekt\n";

exit(1);

}

}

int main()

{

date d;

d.init(29, 11, 2004); // okay

d.init(789, 1, 1999); // Programm-Abbruch

}

Ein wichtiger Grundsatz in C++ ist, dass ein Objekt immer einen sauberen wohldefinierten

Zustand haben sollte.

12.7 Konstruktoren

Wir haben gelernt, dass lokale Variablen einiger Typen bei der Definition rein zufällige

Startwerte bekommen – z.B. alle elementaren Datentypen. Dem gegenüber ist z.B. ein

String immer ein Leerstring, wenn er ohne Argumente erzeugt wird:

int main()

{

int i; // zufaelliger Startwert

string s; // genau definiert -> Leerstring

cout << '"' << s "\" - " << i << '\n';

}

Dies gilt auch, wenn diese Typen in Klassen liegen, und ein Objekt der Klasse als lokale

Variable erzeugt wird:

class A

{

public:

int i;

string s;

};

int main()

{

A a; // a.s ist Leerstring, a.i ist zufaellig

cout << '"' << a.s << "\" - " << a.i << '\n';

}

Da ein Objekt niemals einen instabilen Zustand haben sollte, ist dieses Verhalten schlecht.

Darum ist es möglich, ein Objekt direkt bei der Erstellung sauber zu initialisieren. Hierfür gibt

es in C++ spezielle Element-Funktionen, die Konstruktoren:

Konstruktoren tragen immer den Namen der Klasse.

Sie haben keinen Rückgabewert, auch nicht „void“.

Wird ein Objekt erzeugt, so wird immer automatisch der entsprechende Konstruktor

aufgerufen – dies gilt ohne Ausnahme.

Der entsprechende Konstruktor ergibt sich aus den Argumenten beim Aufruf – hier gelten

die normalen Funktions-Überladen-Regeln.

#include <iostream>

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 126 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

using namespace std;

class A

{

public:

A(); // <= Deklaration Konstruktor (1)

A(int); // <= Deklaration Konstruktor (2)

A(double); // <= Deklaration Konstruktor (3)

A(int, double); // <= Deklaration Konstruktor (4)

void print();

private:

int n_;

double d_;

};

A::A() // <= Definition Konstruktor (1)

{

cout << "A::A()" << endl;

n_ = 0;

d_ = 0.0;

}

A::A(int n) // <= Definition Konstruktor (2)

{

cout << "A::A(int " << n << ')' << endl;

n_ = n;

d_ = 0.0;

}

A::A(double d) // <= Definition Konstruktor (3)

{

cout << "A::A(double " << d << ')' << endl;

n_ = 0;

d_ = d;

}

A::A(int n, double d) // <= Definition Konstruktor (4)

{

cout << "A::A(int " << n << ", double " << d << ')' << endl;

n_ = n;

d_ = d;

}

void A::print()

{

cout << "=> n:" << n_ << " - d:" << d_ << endl;

}

int main()

{

A a1; // <= Nutzung Konstruktor (1)

a1.print(); // => n:0 – d:0

A a2(4); // <= Nutzung Konstruktor (2)

a2.print(); // => n:4 – d:0

A a3(2.7); // <= Nutzung Konstruktor (3)

a3.print(); // => n:0 – d:2.7

A a4(6, 3.1); // <= Nutzung Konstruktor (4)

a4.print(); // => n:6 – d:3.1

}

Ausgabe

A::A()

=> n:0 - d:0

A::A(int 4)

=> n:4 - d:0

A::A(double 2.7)

=> n:0 - d:2.7

A::A(int 6, double 3.1)

=> n:6 - d:3.1

Für Konstruktoren gilt:

Konstruktoren sollen das Objekt sauber konstruieren.

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 127 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Sie dürfen überladen werden.

Es dürfen Default-Argumente benutzt werden.

Ihr vorzeitiges Ende kann mit return (ohne Ausdruck) erreicht werden.

Bis auf ihren speziellen Verwendungszweck, ihrem fehlenden Rückgabetyp und der

fehlenden Adresse sind sie ganz normale Element-Funktionen.

Übertragen auf unsere Klasse „date“ bedeutet dies, dass wir zwei Konstruktoren zur

Verfügung stellen sollten:

Einen Konstruktor ohne Parameter für das aktuelle Datum

Einen Konstruktor mit drei Int-Parametern für die Übergabe von Tag, Monat und Jahr.

class date

{

public:

date(); // <= Deklaration Konstruktor (1)

date(int, int, int); // <= Deklaration Konstruktor (2)

// Rest wie bisher

};

date::date() // <= Definition Konstruktor (1)

{

init();

}

date::date(int d, int m, int y) // <= Definition Konstruktor (2)

{

init(d, m, y);

}

int main()

{

date d1; // <= Nutzung Konstruktor (1)

d1.print(); // => <aktuelles Datum>

date d2(18, 10, 2001); // <= Nutzung Konstruktor (2)

d2.print(); // => 18.10.2001

}

Es gibt mehrere spezielle Konstruktoren bzw. Konstruktor-Familien, die wir in den nächsten

Kapiteln näher besprechen werden:

Standard-Konstruktor

Konvertierungs-Konstruktoren

Kopier-Konstruktor(en)

Move-Konstruktor

Sequenz-Konstruktor

12.7.1 Standard-Konstruktor

Bislang konnte die „date“-Klasse genutzt werden, obwohl sie keinen Konstruktor enthielt.

Dabei hieß es aber eben doch, dass bei jeder Objekterzeugung der entsprechende

Konstruktor aufgerufen wird. Wie funktioniert das denn, wo die Klasse „date“ doch gar

keinen Konstruktor hatte?

Wenn Sie in der Klassen-Defintion keinen einzigen Konstruktor deklarieren, erzeugt der

Compiler automatisch einen public Standard-Konstruktor, der für alle Datenelemente (inkl.

Basis-Klassen) wiederum deren Standard-Konstruktoren aufruft. Dieser Konstruktor heißt:

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 128 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Impliziter Standard-Konstruktor, oder

Automatischer Standard-Konstruktor

class A // Klasse hat keinen user-deklarierten Konstruktor

{ // => Compiler erzeugt impliziten Standard-Konstruktor

public:

void fct();

};

int main()

{

A a; // okay – impliziter Standard-Konstruktor wird genutzt

a.fct();

}

Deklarieren Sie dagegen mindestens einen beliebigen Konstruktor in der Klassen-

Definition, so erzeugt der Compiler keinen Standard-Konstruktor. Benötigen Sie ihn

trotzdem, so müssen Sie ihn dann selber erzeugen.

class A

{

public:

A(int); // User-deklarierte Konstruktor => kein impliziter Standard-Konstruktor

void fct();

};

int main()

{

A a1(1); // Okay

a1.fct();

A a2; // Compiler-Fehler -> kein passender Konstruktor (Standard-Konstruktor)

a2.fct();

}

class A

{

public:

A(); // Expliziter Standard-Konstruktor

A(int); // User-deklarierte Konstruktor => kein impliziter Standard-Konstruktor

void fct();

};

int main()

{

A a1(1); // Okay

a1.fct();

A a2; // Jetzt auch okay, nutzt den expliziten Standard-Konstruktor

a2.fct();

}

In C++ ist der Standard-Konstruktor nicht über die leere Parameterliste, sondern über den

Aufruf definiert: Der Konstruktor, der ohne Argumente aufgerufen werden kann, ist der

Standard-Konstruktor oder auch Default-Konstruktor.

Ein Standard-Konstruktor ist also:

entweder ein Konstruktor ohne Parameter, bzw.

einer, bei dem sämtliche Parameter mit Default-Argumenten belegt sind.

class A

{

public:

A(int = 42); // Dies ist auch ein Standard-Konstruktor

void fct();

};

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 129 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

A::A(int n)

{

cout << "A(" << n << ")\n";

}

int main()

{

A a1; // Aufruf des Standard-Konstruktors mit "42" – Default-Argument

a1.fct();

A a2(6); // Aufruf des Standard-Konstruktors mit "6"

a2.fct();

}

Ausgabe

A(42)

A(6)

Achtung – es gibt in C++ in Verbindung mit dem Standard-Konstruktor eine kleine Falle:

Wollen Sie ein Objekt mit dem Standard-Konstruktor initialisieren, so dürfen Sie keine

runden Klammern verwenden.

class A

{

public:

A();

A(int);

void fct();

};

int main()

{

A a1(6);

A a2(); // Hier liegt der eigentliche Fehler, die Zeile ist aber syntaktisch okay

a1.fct();

a2.fct(); // Compiler-Fehler mit komischer Fehlermeldung vom Compiler

}

Lösung – „A a2()“ ist keine Objektdefinition, sondern eine Funktions-Deklaration der

Funktion „a2“, die keine Parameter erwartet und ein A-Objekt per Kopie zurückgibt.

12.7.2 Temporäre Objekte

Wir können in C++ Konstruktoren explizit aufrufen und damit temporäre Objekte erzeugen.

Temporäre Objekte sind Objekte, die keinen Namen haben und am Ende der Anweisung

automatisch wieder zerstört werden.

class A

{

public:

A(int, int);

};

void fct(const A&);

fct(A(x1, x2));

Der explizite Konstruktor-Aufruf von „A“ erzeugt ein temporäres Objekt von „A“, das keinen

Namen hat, und am Ende der Anweisung automatisch zerstört wird.

Um zu zeigen, dass ein expliziter Konstruktor-Aufruf semantisch nur eine Konvertierung ist

und sich auch entsprechend verhält, erweitern wir das Beispiel etwas. Wir fügen der Klasse

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 130 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

„A“ noch einen Konstruktor mit nur einem Int-Parameter hinzu (damit wir u.a. „static_cast“

nutzen können), und vervollständigen das Beispiel noch mit einigen Ausgaben:

#include <iostream>

using namespace std;

class A

{

public:

A(int); // Konstruktor mit einem "int" – geht auch mit z.B. "static_cast"

A(int, int); // Konstruktor mit zwei "int" – geht nur im funktionalen Stil

void print();

private:

int n1_, n2_;

};

A::A(int n1)

{

n1_ = n1;

n2_ = 0;

}

A::A(int n1, int n2)

{

n1_ = n1;

n2_ = n2;

}

void A::print()

{

cout << "A mit n1:" << n1_ << " - n2:" << n2_ << '\n';

}

void fct(A a) // Achtung – nun als Kopie – eigentlich schlechter – siehe Text (*)

{

cout << "fct(A)\n-> ";

a.print();

}

int main()

{

fct(A(1, 2)); // Explizites temporaeres A-Objekt, funktionaler Stil

fct(A(3)); // Explizites temporaeres A-Objekt, funktionaler Stil

fct((A)4); // Explizites temporaeres A-Objekt, alter C-Stil

fct(static_cast<A>(5)); // Explizites temporaeres A-Objekt, mit "static_cast"

}

Ausgabe

fct(A)

-> A mit n1:1 - n2:2

fct(A)

-> A mit n1:3 - n2:0

fct(A)

-> A mit n1:4 - n2:0

fct(A)

-> A mit n1:5 - n2:0

Wir sehen also, ein temporäres Objekt ist semantisch eigentlich nichts anderes als eine

explizite Konvertierung. Wir haben gelernt, dass viele Konvertierungen auch implizit ablaufen

können – würde das hier auch funktionieren? Und damit kommen wir fließend zum nächsten

Thema – den Konvertierungs-Konstruktoren…

12.7.3 Konvertierungs-Konstruktoren

In C++ kann man benutzer-definierte Konvertierungen definieren, die der Compiler auch für

implizite Konvertierungen nutzen darf. Diese benutzer-definierten Konvertierungen definiert

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 131 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

man entweder mit Konvertierungs-Konstruktoren oder Konvertierungs-Operatoren.

Im Prinzip ist jeder (nicht expliziter) Konstruktor, den man mit einem Argument aufrufen

kann, ein Konvertierungs-Konstruktor, der direkt nutzbar ist:

#include <iostream>

#include <string>

using namespace std;

class A

{

public:

A(int); // Konvertierungs-Konstruktor

A(const string&); // Konvertierungs-Konstruktor

A(bool, double = 3.14); // Konvertierungs-Konstruktor - dank Default-Argument

};

A::A(int n)

{

cout << "A(int: " << n << ")\n";

}

A::A(const string& s)

{

cout << "A(string: " << s << ")\n";

}

A::A(bool b, double d)

{

cout << "A(bool: " << b << ", double: " << d << ")\n";

}

void fct(const A&)

{

}

int main()

{

cout << boolalpha;

string str("C++");

fct(1); // Erzeugt mit Konvertierungs-Konstruktor "A(int)" temporaeres Objekt

fct(str); // Dito mit Konvertierungs-Konstruktor "A(const string&)"

fct(true); // Dito mit Konvertierungs-Konstruktor "A(bool, double=3.14)"

}

Ausgabe

A(int: 1)

A(string: C++)

A(bool: true, double: 3.14)

Möchte man nicht, dass ein Ein-Parameter-Konstruktor als Konvertierungs-Konstruktor zur

Verfügung steht – z.B. um Mehrdeutigkeiten und Fehler zu vermeiden – so kann man ihn

„explicit“ machen.

#include <iostream>

using namespace std;

class A

{

public:

explicit A(int); // <= Kein Konvertierungs-Konstruktor mehr: "explicit"

};

void fct(const A&)

{

}

int main()

{

fct(2); // Compiler-Fehler - kein Konvertierungs-Konstruktor vorhanden

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 132 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

fct(A(3)); // Explizite Konvertierung geht natuerlich weiterhin

}

Im Prinzip sind alle Konstruktoren Konvertierungs-Konstruktoren sein – nicht nur die, die mit

einem Argument aufrufbar sind. Auch die, die mit keinem oder mehreren Argumenten

aufrufbar sind. Damit beim Aufruf klar ist, welche Argumente zusammen ein Objekt bilden

sollen, müssen diese dann in geschweifte Klammern gesetzt werden. Diese impliziten

Konvertierungen sind also nicht direkt nutzbar, sondern benötigen einen Hinweis des

Programmierers.

#include <iostream>

using namespace std;

class A

{

public:

A(int, int); // In C++03 KEIN Konvertierungs-Konstruktor

}; // In C++11 mit {} als solcher nutzbar

A::A(int n1, int n2)

{

cout << "A(n1: " << n1 << ", n2: " << n2 << ")\n";

}

void fct(const A&)

{

}

int main()

{

fct( { 1, 2 } ); // Implizite Konvertierung mit geschweiften Klammern in C++11

}

Ausgabe

A(n1: 1, n2: 2)

Und die implizite Konvertierung mit „{}“ funktioniert auch für einen Standard-Konstruktor:

// Achtung – C++11 Code, funktioniert nur mit einem entsprechenden C++11 Compiler

#include <iostream>

using namespace std;

class A

{

public:

A();

};

A::A()

{

cout << "A()\n";

}

void fct(const A&)

{

}

int main()

{

fct({}); // Aufruf von "A()" durch die geschweiften Klammern "{}"

}

Ausgabe

A()

Auch hier kann man die implizite Konvertierung wieder mit dem Schlüsselwort „explicit“

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 133 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

verhindern:

#include <iostream>

using namespace std;

class A

{

public:

explicit A(int, int); // Kein Konvertierungs-Konstruktor mehr

};

void fct(const A&)

{

}

int main()

{

fct( { 1, 2 } ); // Compiler-Fehler – da Konstruktor "explicit"

fct(A(3, 4)); // Explizite Konvertierung natuerlich weiterhin moeglich

}

12.7.4 Kopier-Konstruktoren

Immer wenn eine Kopie eines Objektes erzeugt wird, wird ein Kopier-Konstruktor (oder

auch „copy-constructor“) der Klasse des Objekts aufgerufen. Kopien werden z.B. erzeugt,

wenn eine Funktion einen Parameter „call-by-value“ erwartet, eine Funktion ein Objekt als

Kopie zurückgibt, oder einfach ein Objekt aus einem anderen erzeugt wird:

void f(std::string); // Parameter-Uebergabe "call-by-value"

std::string g(); // Funktions-Rueckgabe als Kopie

std::string s1;

std::string s2(s1); // Kopie eines Objekts anlegen

Jeder Konstruktor einer Klasse, der mit einem einzelnen Objekt der Klasse aufgerufen

werden kann, ist ein Kopier-Konstruktor.

Ein Kopier-Konstruktor erwartet daher ein Klassen-Objekt als erstes Argument und kann

beliebig viele weitere Parameter haben, die dann aber mit Default-Argumenten belegt

sein müssen.

Ein Kopier-Konstruktor muss das erste Argument natürlich per Referenz bekommen

(sowohl const als auch non-const – normal ist die Const-Referenz).

Er wird benötigt, um ein neues Objekt aus einem bestehenden Objekt zu konstruieren,

z.B. bei einer Objekt-Definition, einem Funktionsaufruf mit call-by-value-Parametern, oder

der Rückgabe eines Objektes bei einer Funktion.

class A

{

public:

A(); // Standard-Konstruktor

A(const A&); // Kopier-Konstruktor

};

A::A()

{

cout << "Standard-Konstruktor\n";

}

A::A(const A&)

{

cout << "Kopier-Konstruktor\n";

}

void f(A) { } // freie Funktion, die eine Kopie erwartet

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 134 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

int main()

{

cout << "Erzeuge a1\n";

A a1;

cout << "Erzeuge a2\n";

A a2(a1); // Kopier-Konstruktor

cout << "Rufe f auf\n";

f(a1); // Kopier-Konstruktor wegen call-by-value

}

Ausgabe

Erzeuge a1

Standard-Konstruktor

Erzeuge a2

Kopier-Konstruktor

Rufe f auf

Kopier-Konstruktor

Hinweis – statt einer Const-Referenz könnte der Kopier-Konstruktor auch mit einer Non-

Const Referenz implementiert werden. Im Normallfall wollen wir bei einer Kopie das Original

aber nicht verändern – ein Kopier-Konstruktor mit Non-Const Referenz ist daher extrem

selten.

12.7.4.1 Automatischer Kopier-Konstruktor

Genauso wenig, wie die Klasse „date“ bislang einen Standard-Konstruktor hatte, hatte sie

auch keinen Kopier-Konstruktor. Trotzdem konnten wir Date-Objekte aus anderen Date-

Objekte erzeugen, bzw. Date-Objekte an Funktionen übergeben.

// Auch bisher war das Kopieren von Date-Objekten kein Problem

void f(date d)

{

d.print(); // => 6.2.2004

}

int main()

{

date d1(6, 2, 2004);

date d2(d1); // Kopier-Konstruktor

f(d2); // Kopier-Konstruktor - wegen call-by-value

}

Der Grund dafür ist ein ähnlicher wie beim Standard-Konstruktor – der Compiler generiert in

vielen Fällen einen automatischen (oder „impliziten“) Kopier-Konstruktor. Der Compiler

erzeugt den automatischen Kopier-Konstruktor, wenn es keinen user-deklarierten Kopier-

Konstruktor gibt.

Der automatische (bzw. implizite) Kopier-Konstruktor:

ist public,

nimmt das Original-Objekt als const-Referenz an, und

ruft für jedes einzelne Element innerhalb der Klasse den jeweiligen Kopier-Konstruktor

auf und erzeugt so das neue Objekt.

Aber wenn der Compiler für Klassen einen Kopier-Konstruktor automatisch erzeugen kann,

wozu dann einen eigenen schreiben? Nun, es gibt Situationen, in denen eine elementweise

Kopie nicht möglich ist, bzw. instabile oder fehlerhafte Zustände liefert. In solchen Fällen

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 135 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

müssen Sie den Kopier-Konstruktor entweder selber implementieren oder ihn verbieten.

Empfehlung – machen Sie sich beim Design und der Entwicklung von Klassen immer

Gedanken darüber, ob der automatische Kopier-Konstruktor ausreichend ist und fehlerfrei

arbeitet. Wenn nicht, müssen Sie selber einen sinnvollen Kopier-Konstruktor entwerfen und

implementieren, oder den Kopier-Konstruktor verbieten. Im Normallfall bezieht sich diese

Überlegung nicht nur auf den Kopier-Konstruktor, sondern auch den Move-Konstruktor, den

Destruktor, den Kopier-Zuweisungs-Operator und den Move-Zuweisungs-Operator – und

mündet dann in der „Regel der 3, 4, 5, 6, 6, 7 und 0“ – die wir hier aber nicht besprechen

werden.

12.7.4.2 Kopier-Konstruktor verbieten

Wie verbietet man den Kopier-Konstruktor einer Klasse? Und damit implizit das Kopieren

von Objekten eines Typs?

Die einfache Lösung in C++ ist die Benutzung von „=delete“.

class A

{

public:

A();

A(const A&) = delete; // Kopier-Konstruktor verboten

};

A::A()

{

}

int main()

{

A a1;

A a2(a1); // Compiler-Fehler, da Kopier-Konstruktor verboten

}

Die Nicht-Implementierung des Kopier-Konstruktor (und später auch des Kopier-

Zuweisungs-Operators) hat oft einen tieferen Hintergrund – häufig verbietet man bei einer

Klasse auch das Kopieren, wenn es semantisch keinen Sinn macht:

Nehmen Sie z.B. an, sie hätten eine Klasse, die in einer grafischen Anwendung den

Maus-Cursor repräsentiert. Was sollte hier passieren, wenn Sie das Maus-Cursor Objekt

kopieren? Bekommen Sie nun einen zweiten Maus-Cursor auf dem Bildschirm?

Wenn man semantisch nicht beschreiben kann, was eine Funktion machen soll – wie will

man sie denn dann implementieren? Die Nicht-Implementierung bewahrt einen also vor

der unlösbaren Aufgabe, etwas nicht spezifierbares umsetzen zu müssen.

Andere Beispiele sind z.B. die Streams, die sich nicht kopieren lassen. Auch hier ist nicht

klar, was passieren sollte, wenn Sie ein File-Stream-Objekt kopieren könnten – sollte

dann die Datei kopiert werden?

12.7.5 Move-Konstruktor

Der Move-Konstruktor soll an dieser Stelle nur grob erwähnt werden. Der Hintergrund für die

sogenannte Move-Semantik sind Objekte, deren Kopien relativ „teuer“ sind (bzgl.

Performance und Speicher-Verbrauch), die sich aber relativ „billig“ verschieben lassen.

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 136 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Beispiele für solche Klassen sind die String-Klasse oder die meisten Container-Klassen.

Deklariert bzw. definiert wird der Move-Konstruktor mit einer Non-Const R-Value Referenz

auf ein Objekt der Klasse. Analog zum Move-Konstruktor gibt es auch noch einen Move-

Zuweisungs-Operator.

class A

{

public:

A(A&&); // Deklaration Move-Konstruktor

A& operator=(A&&); // Deklaration Move-Zuweisungs Operator

};

A::A(A&&) // Definition Move-Konstruktor

{

// Wie auch immer eine sinnvolle Implementierung aussieht...

}

Auch der Move-Konstruktor wird vom Compiler automatisch erzeugt, wenn:

kein benutzer-deklarierter Kopier-Konstruktor,

kein benutzer-deklarierter Kopier-Zuweisungs-Operator,

kein benutzer-deklarierter Move-Konstruktor,

kein benutzer-deklarierter Kopier-Zuweisungs-Operator, und

kein benutzer-deklarierter Destruktor

vorliegt.

Hinweis – während es Standard-, Kopier- und Konvertierungs-Konstruktoren schon in

C++03 gab, ist der Move-Konstruktor eine Neuigkeit von C++11.

12.7.6 Sequenz-Konstruktor

Der Sequenz-Konstruktor ist ein sehr spezieller Konstruktor, der nur selten benötigt wird. Er

ist dann notwendig, wenn man ein Objekt mit einer beliebig großen Menge von Werten eines

Typs initialisieren möchte. Wir kennen dies z.B. von den Containern wie dem Vektor, den wir

mit Werten vorbelegen wollen:

#include <iostream>

#include <vector>

using namespace std;

int main()

{

vector<int> v { 1, 2, 3, 5, 7 }; // Vorbelegung mit einer Menge von Werten

for (int x : v)

{

cout << x << " - ";

}

cout << endl;

}

Ausgabe

1 – 2 – 3 – 5 – 7 -

Möchten wir eine vergleichbare Semantik für unsere eigenen Klasse haben – d.h. die

Initialisierung mit den geschweiften Klammern und einer beliebigen Menge von Werten eines

Typs – dann ist der Sequenz-Konstruktor unser Freund.

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 137 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

12.8 Destruktoren

Analog zu den Konstruktoren gibt es eine spezielle Funktion zum Zerstören eines Objekts -

den Destruktor. Er wird immer automatisch aufgerufen, wenn ein vollständig konstruiertes

Objekt zerstört wird.

Ein Destruktor hat keinen Rückgabewert (auch nicht void).

Er hat keine Parameter.

Sein Name ist der Klassen-Name mit führender Tilde ''..

Eine Klasse hat immer genau einen Destruktor.

Wird er nicht explizit deklariert, so erzeugt der Compiler einen impliziten Destruktor.

class A

{

public:

A(int);

~A();

private:

int i;

};

A::A(int n)

{

i=n;

cout << "Konstruktor " << i << '\n';

}

A::~A()

{

cout << "Destruktor " << i << '\n';

}

int main()

{

cout << "Start\n";

A a1(7);

{

cout << "Start neuer Block\n";

A a2(3);

cout << "Ende neuer Block\n";

} // <- Destruktoraufruf fuer a2

cout << "Ende\n";

} // <- Destruktoraufruf fuer a1

Ausgabe

Start

Konstruktor 7

Start neuer Block

Konstruktor 3

Ende neuer Block

Destruktor 3

Ende

Destruktor 7

Die Aufgabe eines Destruktors ist es, das Objekt sauber abzubauen.

Wird ein Objekt zerstört, so wird zuerst der Destruktor aufgerufen und dann der

Speicherplatz freigegeben – genau umgekehrt zu den Konstruktoren.

Ein impliziter Destruktor ist immer public und ruft für alle Attribute und Basis-Klassen

seinerseits die Destruktoren auf.

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 138 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Der implizite Destruktor für die Klasse ‘date’ macht nichts, da sie nur int-Variablen enthält,

die – wie alle elementaren Datentypen – leere Destruktoren haben.

Machen Sie sich beim Design und der Entwicklung von Klassen immer Gedanken

darüber, ob der implizite Destruktor ausreichend ist und fehlerfrei arbeitet. Wenn nicht,

müssen Sie selber einen sinnvollen Destruktor entwerfen und implementieren.

12.9 Const-Element-Funktionen

Nach dem bis herigem Wissen wäre folgendes richtig, liefert aber einen Compiler-Fehler.

int main()

{

const date d;

d.print(); // Compiler-Fehler

}

Warum aber gibt der Compiler einen Fehler aus? Wir können doch:

ein konstantes Datum definieren

und print() lief bislang problemlos

Problem des Compilers

d ist konstantes date Objekt

Aber es könnte sein, dass print() das Objekt verändert

Lösung

Wir wissen, das ‘print()’ das Objekt nicht ändert, der Compiler aber nicht. Darum müssen wir

dies dem Compiler mittteilen. Dafür wird das Schlüsselwort const sowohl hinter die Element-

Funktions-Deklaration, als auch hinter den Kopf der Element-Funktions-Definition

geschrieben.

class date

{

...

void print() const; // hier ein const

...

};

void date::print() const // hier auch ein const

{

...

}

int main()

{

const date d;

d.print(); // jetzt okay

}

Das Schlüsselwort const hinter einer Element-Funktion besagt, dass diese Element-

Funktion das Objekt nicht ändert. Denken Sie daran, dass const nach links bindet, und links

steht quasi das Objekt.

Versucht eine Const-Element-Funktion ein Objekt zu ändern, gibt der Compiler natürlich

einen Fehler aus.

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 139 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

void date::print() const

{

++year_; // Compiler-Fehler

...

}

In einer const-Element-Funktion können wiederum auch nur Element-Funktionen

aufgerufen werden, die selbst als const deklariert sind.

class A

{

public:

void fct() const;

void fct_is_const() const;

void fct_is_not_const();

};

void A::fct() const

{

fct_is_const(); // okay, da eine const Element-Funktion

fct_is_not_const(); // Compiler-Fehler, da nicht const

}

12.9.1 const gehört zum Funktions-Namen

Das Schlüsselwort const gehört wie die Signatur (Name + Parameterliste) zum

Funktionsnamen.

Konsequenz – es kann zwei bis auf const vom Funktions-Namen und der Parameterliste

her identische Element-Funktionen geben. Die Entscheidung, welche Funktion vom

Compiler aufgerufen wird, trifft er anhand von Überladenregeln bezogen auf das aktuelle

Objekt. Für const Objekte wird die const Element-Funktion, für non-const Objekte die non-

const Element-Funktion aufgerufen.

class A

{

public:

void f();

void f() const;

};

void A::f() // Definition der 'normalen' Version

{

cout << "normale Version\n";

}

void A::f() const // Definition der const-Version

{

cout << "const Version\n";

}

int main()

{

A a;

const A ca;

a.f(); // ruft die 'normale' Version auf

ca.f(); // ruft die const Version auf

}

Ausgabe

normale Version

const Version

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 140 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Hinweise:

Eine const Element-Funktion kann natürlich auch für non-const Objekte aufgerufen

werden, und wird es auch, wenn keine const-Funktion exisitert.

Zwei bis auf const vom Funktions-Namen und der Parameterliste her identische Element-

Funktionen dürfen unterschiedliche Rückgabe-Typen haben, da es zwei gänzlich

unabhängige Funktionen sind.

Empfehlung – machen Sie jede Element-Funktionen const, bei der das möglich ist. Sie

schränken sonst die Benutzung ihrer Klassen unnötig ein – z.B. bei der typischen Übergabe

eines Objekts an eine Funktion mit „const type&“ können nur const-Element-Funktionen für

das Objekt aufgerufen werden.

12.10 this

In jeder Element-Funktion ist automatisch ein Zeiger auf das aktuelle Objekt definiert,

repräsentiert durch das Schlüsselwort this. Da wir Zeiger nicht kennen, nehmen wir das

erstmal so hin. Merken sie sich aber, dass - ähnlich zu Iteratoren - das dereferenzierte

„this“, d.h. „*this“ immer das aktuelle Objekt selber ist, d.h. das Objekt für das die Element-

Funktion aufgerufen wurde.

class A

{

public:

A(int);

A& f1();

void f2();

private:

int n_;

};

A::A(int n)

{

n_ = n;

}

A& A::f1()

{

cout << "f1:" << n_++ << '\n';

return *this;

}

void A::f2()

{

cout << "f2:" << n_ << '\n';

}

int main()

{

A a(4);

a.f2();

a.f1().f2();

}

Ausgabe

f2:4

f1:4

f2:5

Der this-Zeiger wird in der Praxis benutzt um z.B.:

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 141 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

die Adresse des aktuellen Objekts zu ermitteln – z.B. bei Objektvergleichen,

um das aktuelle Objekt selber zurückzugeben – z.B. um Funktionsaufrufe zu verketten,

um das aktuelle Objekt an andere Funktionen übergeben zu können.

12.11 Inline

Auch in Bezug auf inline sind Element-Funktionen ganz normale Funktionen:

entweder schreiben sie das Schlüsselwort inline vor Deklaration und Definition, oder

sie implementieren die Definition direkt in der Klassen-Definition (in diesem Fall ist das

Schlüsselwort gar inline nicht nötig - dies wird auch „impliztes Inline“ genannt - s.u.

class date

{

public:

int day() const { return day_; } // implizites inline ohne Semikolon

int month() const { return month_; }; // implizites inline mit Semikolon

inline int year() const; // explizites inline

// Rest wie bisher...

};

inline void date::year() const

{

return year_;

}

Bemerkung - werden Element-Funktionen direkt in der Klassendefiniton definiert, so erlaubt

die Syntax von C++, dass das abschliessende Semikolon wegfallen kann, da die

abschliessende geschweifte Klammer die Definition eindeutig beendet – siehe Beispiel.

12.12 Klassen verwenden Klassen

Klassen können natürlich selber wieder als Attribute eingesetzt werden:

class person

{

public:

person(const date&);

private:

date birthday_;

};

person:: person(const date& birthday)

{

birthday_ = birthday;

}

Wird ein Objekt erzeugt, so wird defaultmäßig:

1. Speicher reserviert,

2. die Standard-Konstruktoren der Attribute in der Reihenfolge der Deklarationen, d.h.

ihrem Vorkommen in der Klassen-Definition aufgerufen, und

3. der Konstruktor der Klasse selber durchlaufen (Konstruktor-Rumpf).

Wird ein Objekt zerstört, ist die Reihenfolge genau umgekehrt, d. h.

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 142 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

1. wird der Destruktor der Klasse durchlaufen,

2. werden die Destruktoren der Attribute in der umgekehrten Reihenfolge der Deklaration

aufgerufen, und

3. wird der Speicher freigegeben.

Mit dieser Strategie wird sichergestellt, dass Objekte „Ebene für Ebene“ konstruiert werden.

Damit setzt die aktuelle Ebene immer nur auf vollständig fertige Ebenen auf, d.h. kann nur

auf Objekte zugreifen, die einen stabilen Objektzustand erreicht haben.

12.13 Member-Initialisierungs-Listen

Probleme

Die Konstruktion eines Objekts wie im Beispiel in ist nicht optimal, denn:

Performance - erst wird das Attribut mit dem Standard-Konstruktor aufwändig initialisiert,

direkt danach wird es auf einen neuen Wert gesetzt.

Was, wenn Attribute keinen Standard-Konstruktor haben?

Wie können const- oder Referenz- Attribute initialisiert werden?

Lösung

Member-Initialisierungs-Listen

Syntax:

Konstruktorkopf : Member-Initialisierungs-Liste { Konstruktorrumpf }

class A

{

public:

A(int, const double&, const date&);

private:

int i;

double d1;

double d2;

date da1;

date da2;

};

A::A(int v1, const double& v2, const date& v3)

: i(v1), d2(2*v2), da1(v3), da2()

{

}

Für die in der Member-Initialisierungs-Liste aufgeführten Attribute werden die angegebenen

Konstruktoren statt der Standard-Konstruktoren aufgerufen – es kann natürlich auch der

Standard-Konstruktor angegeben werden, siehe im Beispiel das Attribut „da2“.

12.13.1 Attribute ohne Standard-Konstruktor

Attribute, die keinen Standard-Konstruktor haben, müssen in der Member-Initialisierungs-

Liste aufgeführt werden – dies gilt auch für Basis-Klassen.

class A

{

public:

A(int);

};

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 143 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

class B

{

public:

B();

B(int);

private:

A a;

};

B::B() // Compiler-Fehler - Attribut a kann nicht initialisiert werden

{

}

B::B(int arg) // okay - explizite Angabe des int-Konstruktors von A

: a(arg)

{

}

12.13.2 Objekt-Konstanten bzw. const Attribute

Const Attribute müssen in der Member-Initialisierungs-Liste aufgeführt werden, ausser sie

können ohne expliziten Konstruktor-Aufruf erzeugt werden.

class A

{

public:

A();

A(int);

private:

const int ci;

};

A::A() // Compiler-Fehler - const Attribut ci wird nicht initialisiert

{

}

A::A(int arg) // okay

: ci(2*arg+7)

{

}

12.13.3 Referenz-Attribute

Referenz-Attribute müssen in der Member-Initialisierungs-Liste initialisiert werden.

class A

{

public:

A();

A(const date&);

private:

const date& date_;

};

A::A() // Compiler-Fehler - Referenz wird nicht initialisiert

{

}

A::A(const date& d) // okay

: date_(d)

{

}

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 144 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

12.13.4 Typischer Fehler

Das folgende Beispiel stellt eine Klasse für einen Kreis dar. Aus Performancegründen wird in

dieser Klasse sowohl der Umfang als auch der Radius gespeichert, damit die Berechnung

nur einmal erfolgen muss.

Diese Klasse enthält einen Fehler, welchen?

Was gibt die Element-Funktion „print() const“ für das Objekt „limit“ aus?

class circle

{

public:

circle(double);

void print() const;

private:

double circumference_;

double radius_;

};

circle::circle(double radius)

: radius_(radius), circumference_(2*3.1415926*radius_)

{

}

void circle::print() const

{

cout << "Kreis mit Radius " << radius_

<< " und Umfang " << circumference_

<< '\n';

}

int main()

{

circle limit(4.0);

limit.print();

}

Fehler - da die Attribute in der Reihenfolge der Deklaration konstruiert werden – wird

„circumference_“ vor „radius_“ mit einem zu dem Zeitpunkt rein zufälligen Wert für den

Radius erzeugt. Die Anordnung in der Member-Initialisierungs-Liste spielt keine Rolle für die

Reihenfolge der Konstruktor-Aufrufe der Attribute.

12.14 Klassen-Deklarationen

Zwischen einzelnen Klassen können Ring-Abhängigkeiten herrschen:

A braucht B und B braucht A.

Da der Compiler nur bekannte Klassen verwenden kann, können Klassen mit dem

Schlüsselwort class und dem Klassen-Namen deklariert werden.

class B; // Macht die Klasse B für den Compiler bekannt

class A

{

public:

int fct(const B&); // Benutzung der Klasse B als Referenz-Parameter

};

class B // Deklaration Klasse B

{

public:

A a;

};

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 145 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Die Klassen-Deklaration funktioniert nur, solange der Compiler keine näheren Angaben über

die deklarierte Klasse benötigt, z.B. Größe oder internen Aufbau.

class B;

class A

{

public:

A();

void f(B*); // okay

void f(B&); // okay

void f(B); // okay

B* g(); // okay

B& g(); // okay

B g(); // okay

private:

B* p; // okay

B& r; // okay

B b; // Compiler-Fehler

};

Eine Klassen-Deklaration reicht aus für:

Funktions-Parameter und Funktions-Rückgaben in Deklarationen, da der Compiler hier

keinen Code erzeugt, sondern hier nur eine Funktion deklariert wird.

Zeiger- und Referenz-Attribute, da deren Grösse unabhängig vom Aufbau der

referenzierten Klasse ist, und dem Compiler die Grösse bekannt ist.

Für Wert-Attribute muss die Deklaration der Attribut-Klasse bekannt sein, da der Compiler

z.B. die Grösse der Klasse wissen muss.

12.15 Klassen-Elemente

Klassen-Elemente sind klassenspezifische Elemente, die keinem Objekt sondern der

Klasse zugeordnet sind.

Es gibt:

Klassen-Variablen, und

Klassen-Funktionen.

12.15.1 Klassen-Variablen

Eine Klassen-Variablen ist eine der Klasse zugeordnete Variable, die:

nur einmal im Programm existiert, unabhängig von der Anzahl instanziierter Objekte,

und den normalen Zugriffsrechten der Klasse unterliegt.

Angesprochen wird sie:

von innerhalb der Klasse ganz normal über ihren Namen, und

von ausserhalb mit zusätzlichem Objekt- oder Klassenbezug.

Klassen-Variablen müssen in der Klasse deklariert, und einmal im Programm (ausserhalb

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 146 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

der Klasse) definiert werden:

Syntax

Deklaration: static typ name;

Definition: typ klasse::name { „(Konstruktor-Argument-Aufrufliste)“ | „= Initialisierer“ };

class A

{

public:

void fct();

static int si;

};

int A::si = 8; // Definition mit Initialisierung

void A::fct()

{

cout << si << '\n'; // direkter Zugriff, da innerhalb der Klasse

}

int main()

{

cout << A::si << '\n'; // Zugriff ueber den Klassen-Namen

A a;

cout << a.si << '\n'; // Zugriff ueber ein Objekt

a.fct();

}

12.15.2 Klassen-Funktionen

Analog zu Klassen-Variablen gibt es Klassen-Funktionen, die ebenfalls nicht einem Objekt,

sondern der Klasse zugeordnet sind, und auch den normalen Zugriffsrechten der Klasse

unterliegen.

Angesprochen werden sie:

von innerhalb der Klasse ganz normal über ihren Namen, und

von ausserhalb mit zusätzlichem Objekt- oder Klassenbezug.

Klassen-Funktionen müssen in der Klasse mit static deklariert werden. Die Definition erfolgt

analog zu den normalen Element-Funktionen.

class A

{

public:

static void fct();

};

void A::fct()

{

cout << "static A::fct()\n";

}

int main()

{

A::fct();

A a;

a.fct();

}

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 147 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

12.16 friend

Mit dem Schlüsselwort friend kann freien Funktionen und Klassen erlaubt werden auf alle

Elemente einer anderen Klasse zuzugreifen - auch die privaten. Sie werden quasi zu

Freunden der Klasse.

12.16.1 Freie friend-Funktionen

Damit eine freie Funktion auf alle Elemente einer Klasse zugreifen kann, muss sie innerhalb

der Klasse mit friend deklariert, d. h. zum Freund der Klasse gemacht werden.

An der Deklaration mit dem Schlüsselwort friend erkennt der Compiler, dass es sich nicht

um eine Element-Funktion, sondern um eine freie Funktion handelt.

class A

{

public:

A(int i) : n(i) {}

friend int fct(const A&); // Achtung - keine Element-Funktion,

// sondern eine freie Funktion

private:

int n;

};

int fct(const A& a) // Definition der freien Funktion fct

{ // Da friend von A, darf sie auf alle

return a.n; // Elemente von A zugreifen

}

int main()

{

A a(17);

cout << fct(a) << '\n'; // Ausgabe: 17

}

Hinweis - noch einmal: obwohl „fct“ innerhalb der Klasse „A“ deklariert wurde, ist „fct“ keine

Element-Funktion, sondern aufgrund von friend eine ganz normale freie Funktion, die eben

nur zusätzlich auf alle Elemente der befreundeten Klasse zugreifen kann.

12.16.2 friend-Klassen

Um eine komplette Klasse zum Freund einer anderen zu machen, muss die Klasse als

Vorwärts-Deklaration mit friend in der Klassendefinition aufgeführt werden. Damit darf

innerhalb der befreundeten Klasse auf alle Elemente der Freund-Klasse zugegriffen werden.

class B;

class A

{

public:

int f(const B&) const;

};

class B

{

friend class A; // macht A zum friend von B

public:

B(int i) : n(i) {}

private:

int n;

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 148 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

};

int A::f(const B& b) const // Definition der Element-Funktion A::f

{ // Da A friend von B ist, duerfen alle

return b.n; // Funktionen von A auf alle Elemente

} // von B zugreifen

int main()

{

A a;

B b(42);

std::cout << a.f(b) << '\n'; // Ausgabe: 42

}

12.16.3 Weiteres

friend-Beziehungen sind nicht transitiv

A friend von B und B friend von C daraus folgt nicht: A friend von C.

class A

{

friend class B;

int i;

};

class B

{

friend class C;

};

class C

{

public:

void fct(A& a) { a.i++; } // Compiler-Fehler - C ist kein Freund von A

};

Bemerkung - friend-Beziehungen werden auch nicht vererbt.

12.17 Klassenbezogene Typen

In Klassen können nicht nur Element-Funktionen, Konstruktoren, Destruktoren, Attribute,

Klassen-Funktionen und Klassen-Variablen definiert werden, sondern auch Typen, z.B.:

Typ-Aliase mit using oder typedef

Aufzählungstypen mit enum

Innere Klassen

Diese Typen unterliegen den normalen Zugriffsbereichen der Klasse, d.h. private Typen

können nur innerhalb der Klasse benutzt werden, während public Typen überall benutzbar

sind.

Angesprochen werden die Typen:

von innerhalb der Klasse ganz normal über ihren Namen, und

von ausserhalb mit zusätzlichem Klassenbezug.

class paragraph

{

public:

enum class alignment { left, center, right };

alignment get_alignment() const { return alignment_; }

void set_alignment(alignment arg) { alignment_ = arg; }

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 149 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

private:

using length = long;

class word

{

public:

const string& value() const;

private:

string value_;

};

alignment alignment_;

length length_;

};

const string& paragraph::word::value() const

{

return value_;

}

int main()

{

paragraph para;

para.set_alignment(paragraph::alignment::center);

paragraph::alignment al = para.get_alignment();

paragraph::alignment a; // okay, da public

paragraph::lenght l; // Compiler-Fehler, da private

paragraph::word w; // Compiler-Fehler, da private

}

13 Operator-Funktionen

13.1 Einführung

In einer fiktiven Klasse „rational“ (bruch) müßten bislang mathematische Operationen als

Element-Funktionen abgebildet werden - z.B. um Brüche zu multiplizieren. Die möglichen

Element-Funktionen wären z.B. „mul“ für die Multiplikation oder „mul_assign“ für die

multiplikative Zuweisung, deren Verhalten der normalen Operator-Semantik von „*=“ bzw. „*“

z.B. bei Ints entsprechen würden. Für den Benutzer der Klassen wäre es sicher

angenehmer, wenn er statt der Element-Funktionen diese Operatoren zur Verfügung hätte.

Bisheriges Wissen Schöner wäre

rational r1, r2, r3;

r1 = r2.mul(r3);

rational r1, r2, r3;

r1 = r2*r3;

rational r1, r2;

r1.mul_assign(r2);

rational r1, r2;

r1 *= r2;

In C++ können die meisten der vorhandenen Operatoren überladen werden. Ein

Operator ist im Prinzip eine ganz normale freie Funktion oder eine ganz normale

Element-Funktion - bis auf:

Operatoren müssen mit dem Schlüsselwort operator und dem Operator selber als Name

deklariert und definiert werden.

Es muss mindestens ein Parameter ein benutzerdefinierter Typ sein.

Der Aufruf einer Operator-Funktion ist sowohl in Funktions-Schreibweise, als auch in

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 150 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Operator-Schreibweise möglich – siehe folgendes Beispiel.

Die Anzahl der Parameter ist durch den Operator festgelegt – Ausnahme Aufruf-

Operator. Z.B. der binäre Operator „+“ (die Addition) hat zwei Operanden – und das

können Sie nicht ändern.

Im Normallfall sind keine Default-Argumente zugelassen, da sie die syntaktische

Eindeutigkeit verletzen würden (Ausnahme: Aufruf-Operator).

Die genaue Syntax der Deklaration und Definition ist von der Verwendung des Operators

als freie Funktion oder als Element-Funktion abhängig.

Syntax

Dekl.: Rückgabetyp operator @ (Parameterliste);

Def.: Rückgabetyp operator @ (Parameterliste) { Funktionsrumpf }

oder Rückgabetyp Klassen-Name::operator @ (Parameterliste) { Funktionsrumpf }

Hinweis – das „@“ steht hier für der erlaubten Operatoren wie z.B. „+“ oder „*“.

Beispiel für die Element-Operator-Funktion „*“ in einer Bruch Klasse

#include <iostream>

using namespace std;

class rational

{

public:

rational(int n=0, int d=1) : numerator_(n), denominator_(d) {}

rational mul(const rational&) const; // bisheriges Wissen

rational operator*(const rational&) const; // neues Wissen

void print() const { cout << numerator_ << '/' << denominator_; }

private:

int numerator_, denominator_;

};

// Man koennte die beiden Funtionen auch problemlos in einer Zeile implementieren.

// Hier ist es nicht geschehen, um klar zu zeigen, was in ihr passiert.

//

// So wuerden die Funktionen in einer Zeile aussehen:

// return rational(numerator_ * rhs.numerator_, denominator_ * rhs.denominator_);

//

// Ausserdem sind die Funktionen gute Kandidaten fuer inline-Funktionen.

// Bisheriges Wissen

rational rational::mul(const rational& rhs) const

{

int n = numerator_ * rhs.numerator_;

int d = denominator_ * rhs.denominator_;

rational result(n, d);

return result;

}

// Neues Wissen

rational rational::operator*(const rational& rhs) const

{

int n = numerator_ * rhs.numerator_;

int d = denominator_ * rhs.denominator_;

rational result(n, d);

return result;

}

int main()

{

rational r, r1(2, 3), r2(4, 5);

r = r1.mul(r2); // Bisheriges Wissen

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 151 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

r.print();

cout << endl;

r = r1.operator*(r2); // Operator-Aufruf in Funktions-Schreibweise

r.print();

cout << endl;

r = r1 * r2; // Operator-Aufruf in Operator-Schreibweise

r.print();

cout << endl;

}

Ausgabe

8/15

8/15

8/15

Ich hoffe, Sie sehen wie gleich unsere bisherige Lösung mit Element-Funktion „mul“ und die

neue Operator-Funktion „*“ ist. Abgesehen vom Namen und dem möglichen Aufruf in

Operator-Schreibweise gibt es keinen Unterschied.

Beispiel für die freie Operator-Funktion „*“ für eine Bruch-Klasse

#include <iostream>

using namespace std;

class rational

{

public:

rational(int n=0, int d=1) : numerator_(n), denominator_(d) {}

// Jetzt benoetigen wir Getter fuer Zaehler und Nenner

// Oder wir machen die Funktionen zu Friends

int numerator() const { return numerator_; }

int denominator() const { return denominator_; }

void print() const { cout << numerator_ << '/' << denominator_; }

private:

int numerator_, denominator_;

};

// Deklarationen der freien Funktionen

rational mul(const rational&, const rational&);

rational operator*(const rational&, const rational&);

// - Definitionen der freien Funktionen

// - Die Implementierung waere natuerlich auch hier in einer Zeile moeglich:

// return rational(lhs.numerator()*rhs.numerator(),lhs.denominator()*rhs.denominator());

// - Als 'friend' Funktion koennte man sich die Get-Funktionen sparen,

// was sicher sinnvoller waere.

// - Und natuerlich wieder ein super Kandidat fuer eine inline-Funktion.

rational mul(const rational& lhs, const rational& rhs)

{

int n = lhs.numerator() * rhs.numerator();

int d = lhs.denominator() * rhs.denominator();

rational result(n, d);

return result;

}

rational operator*(const rational& lhs, const rational& rhs)

{

int n = lhs.numerator() * rhs.numerator();

int d = lhs.denominator() * rhs.denominator();

rational result(n, d);

return result;

}

int main()

{

rational r, r1(2, 3), r2(4, 5);

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 152 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

r = mul(r1, r2); // Bisheriges Wissen

r.print();

cout << endl;

r = operator*(r1, r2); // Operator-Aufruf in Funktions-Schreibweise

r.print();

cout << endl;

r = r1 * r2; // Operator-Aufruf in Operator-Schreibweise

r.print();

cout << endl;

}

Ausgabe

8/15

8/15

8/15

Beide Lösungen sind prinzipiell identisch – aber beide haben ihre Vorteile:

Element-Operator-Funktionen:

Sie sind semantisch eindeutig der Klasse zugeordnet.

Sie haben Zugriff auf alle Elemente der Klasse.

Sie können überschrieben werden

Freie Operator-Funktionen:

Sie können symmetrisch arbeiten

Sie können für fremde Klassen definiert werden

Sie können für Enums definert werden.

Sie müssen aber oft als friend deklariert werden, oder mit einer Indirektion implementiert

sein, um ihre Aufgabe erfüllen zu können.

13.2 Symmetrische Operatornutzung

Element-Operator-Funktionen können nur mit einem Objekt als erstem Argument aufgerufen

werden. Freie Operator-Funktionen können dagegen auch für das erste Argument die

implizite Typumwandlung nutzen - damit kann der Operator flexibler genutzt werden.

class A // Klasse A mit freier Operator-Funktion +

{

public:

A();

A(int);

};

A operator+(const A&, const A&);

class B // Klasse B mit Element-Operator-Funktion +

{

public:

B();

B(int);

B operator+(const B&) const;

};

int main()

{

A a1, a2, a3;

a1 = a2 + a3; // okay -> a1 = operator(a2, a3)

a1 = 1 + a3; // okay -> a1 = operator(A(1),a3)

a1 = a2 + 2; // okay -> a1 = operator(a2, A(2))

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 153 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

a1 = 3 + 4; // okay -> a1 = A(3+4)

B b1, b2, b3;

b1 = b2 + b3; // okay -> b1 = b2.operator(b3)

b1 = 1 + b3; // Compiler-Fehler -> b1 = B(1).operator(b3) nicht machbar

b1 = b2 + 2; // okay -> b1 = b2.operator(B(2))

b1 = 3 + 4; // okay -> b1 = B(3+4)

}

Empfehlung – benutzen Sie, wenn möglich, eine Element-Operator-Funktion. Bei Klassen

mit impliziter Typumwandlung und Operatoren, die symmetrisch aufgerufen können werden

sollen, muss es dagegen eine freie Operator-Funktion sein.

13.3 Ausgabe

Bislang mussten wir folgendes schreiben:

date d;

d.print();

Schöner wäre aber die normale Ausgabe mit:

date d;

std::cout << d;

Lösung – ein entsprechender Operator muss definiert werden.

Wir definieren den Ausgabe-Operator für die Klasse „date“. Da

der erste Parameter des Ausgabe-Operators „<<“ der Stream ist, und

die Klasse „std::ostream“ – als Klasse der C++ Standard-Bibliothek – von uns nicht

erweitert werden kann,

=> muss der Ausgabe-Operator als freie Operator-Funktion implementiert werden.

Und damit der Ausgabe-Operator Zugriff auf die Attribute der Klasse „date“ hat, wird er

als Friend-Funktion deklariert

#include <ctime>

#include <iomanip>

#include <iostream>

using namespace std;

class date

{

public:

date();

date(int d, int m, int y);

friend ostream& operator<<(ostream&, const date&); // Deklaration Ausgabe-Operator

private:

int day_;

int month_;

int year_;

};

date::date()

{

time_t timer = time(0);

tm* tblock = localtime(&timer);

day_ = tblock->tm_mday;

month_ = tblock->tm_mon+1;

year_ = tblock->tm_year+1900;

}

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 154 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

date::date(int d, int m, int y)

: day_(d), month_(m), year_(y)

{

}

ostream& operator<<(ostream& out, const date& d) // Ausgabe-Operator

{

char c = out.fill('0');

out << setw(2) << d.day_ << '.' << setw(2) << d.month_ << '.' << setw(4) << d.year_;

out.fill(c);

return out;

}

int main()

{

date d1;

cout << "Es ist der " << d1 << '\n';

date d2(31, 12, 2000);

cout << "Das letzte Jahrtausend endete am " << d2 << '\n';

}

Mögliche Ausgabe (da das aktuelle Datum vorkommt)

Es ist der 13.01.2013

Das letzte Jahrtausend endete am 31.12.2000

Dieser Ausgabe-Operator funktioniert auch mit Ausgabe-File-Streams oder String-Streams.

Der Grund dahinter ist die „ist-ein“ Beziehungs-Semantik von öffentlicher Vererbung.

13.4 Kopier-Zuweisungs-Operator =

Der Kopier-Zuweisungs-Operator „=“ ist der Zuweisungs-Operator „=“, der ein Objekt der

Klasse selber erwartet. Er hat eine ähnliche Sonderstellung wie der Kopier-Konstruktor.

Im Normallfall sind in C++ Objekte einander zuweisbar. Vom Compiler wird daher, wenn Sie

selber keinen Kopier-Zuweisung-Operator deklarieren, automatisch ein impliziter „public“

Kopier-Zuweisungs-Operator erzeugt, der ein const-Referenz-Objekt der Klasse erwartet, für

jedes Attribut der Klasse wiederum den Zuweisungs-Operator aufruft, und eine Referenz auf

das aktuelle Objekt zurückgibt.

Reicht der automatische Kopier-Zuweisungs-Operator nicht aus, so müssen wir selber einen

definieren. Typischerweise ist er „public“, erwartet das zugewiesene Objekt per Const-

Referenz und gibt das aktuelle Objekt als Referenz zurück – verhält sich also vergleichbar

zum automatischen Kopier-Zuweisungs-Operator.

class A

{

public:

A& operator=(const A&);

};

A& A::operator=(const A&)

{

cout << "Kopier-Zuweisungs-Operator\n";

return *this;

}

int main()

{

A a1, a2;

a1 = a2; // Ausgabe: Kopier-Zuweisungs-Operator

}

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 155 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Achtung – der Zuweisungs-Operator muss immer als Element-Operator-Funktion definiert

sein. Er darf nicht als freie Operator-Funktion implementiert werden.

Achtung – machen Sie sich bitte den Unterschied zwischen einem Kopier-Konstruktor und

einem Kopier-Zuweisungs-Operator klar. Obwohl beide sehr ähnlich wirken, sind ihre

Funktionen doch recht verschieden:

Ein Kopier-Konstruktor erzeugt ein neues Objekt aus einem bestehenden.

Ein Kopier-Zuweisungs-Operator weist ein bestehendes Objekt einem bestehenden zu.

13.4.1 Kopier-Zuweisungs-Operator verbieten

Wie verbietet man den Kopier-Zuweisungs-Operator? Oder genauer: wie verbietet man das

Zuweisen von Typen?

class A

{

public:

A();

A(const A&) = delete; // Kein Kopier-Konstruktor

A& operator=(const A&) = delete; // Kein Kopier-Zuweisungs-Operator

};

int main()

{

A a1, a2;

A a3(a1); // Compiler-Fehler, da A nicht kopierbar

a1 = a2; // Compiler-Fehler, da A nicht zuweisbar

}

13.5 Move-Zuweisungs-Operator =

So wie der Kopier-Konstruktor der Bruder des Kopier-Zuweisungs-Operators ist, so gibt es in

C++11 natürlich auch einen Bruder zum Move-Konstruktor – den Move-Zuweisungs-

Operator.

class A

{

public:

A(A&&); // Deklaration Move-Konstruktor

A& operator=(A&&); // Deklaration Move-Zuweisungs Operator

};

A& A::operator=(A&&) // Definition Move-Zuweisungs Operator

{

// Wie auch immer eine sinnvolle Implementierung aussieht...

}

Auch der Move-Zuweisungs-Operator wird vom Compiler automatisch erzeugt, wenn:

kein benutzer-deklarierter Kopier-Konstruktor,

kein benutzer-deklarierter Kopier-Zuweisungs-Operator,

kein benutzer-deklarierter Move-Konstruktor,

kein benutzer-deklarierter Kopier-Zuweisungs-Operator, und

kein benutzer-deklarierter Destruktor

vorliegt.

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 156 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Detaillierter wird hier nicht auf den Move-Zuweisungs-Operator und die Move-Semantik

eingegangen – das sprengt den Rahmen der Vorlesung.

13.6 Funktions-Aufruf Operator

Ein ganz spezieller Operator ist in C++ der Funktions-Aufruf-Operator „()“ – die runden

Klammern. Mit ihnen kann man einem Objekt Funktions-Charakter geben, d.h. Objekte wie

Funktionen nutzen.

#include <iostream>

using namespace std;

class A

{

public:

void operator()() const;

};

void A::operator()() const

{

cout << "Funktions-Aufruf-Operator ()" << endl;

}

int main()

{

A a;

a(); // Keine Funktion – Aufruf des Operators "()" fuer das Objekt "a"

}

Ausgabe

Funktions-Aufruf-Operator ()

Der Funktions-Aufruf-Operator ist der einzige Operator in C++, bei dem der Operator die

Anzahl an Parametern nicht festlegt und auch Default-Argumente erlaubt sind – d.h. der

Funktions-Aufruf-Operator darf mit beliebigen Parameter-Anzahlen definiert werden und es

dürfen dabei Default-Argumente benutzt werden.

#include <iostream>

using namespace std;

class A

{

public:

void operator()() const;

void operator()(int) const;

void operator()(bool) const;

void operator()(double, int=5) const;

};

void A::operator()() const

{

cout << "Funktions-Aufruf-Operator ()" << endl;

}

void A::operator()(int n) const

{

cout << "Funktions-Aufruf-Operator (int " << n << ")" << endl;

}

void A::operator()(bool b) const

{

cout << "Funktions-Aufruf-Operator (bool " << b << ")" << endl;

}

void A::operator()(double d, int n) const

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 157 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

{

cout << "Funktions-Aufruf-Operator (double " << d << ", int " << n << ")" << endl;

}

int main()

{

A a;

a(); // Aufruf des Operators "()"

a(4); // Aufruf des Operators "(int)" mit "4"

a(true); // Aufruf des Operators "(bool)" mit "true"

a(2.72); // Aufruf des Operators "(double, int)" mit "2.72, 5"

a(3.14, 6); // Aufruf des Operators "(double, int)" mit "3.14, 6"

}

Ausgabe

Funktions-Aufruf-Operator ()

Funktions-Aufruf-Operator (int 4)

Funktions-Aufruf-Operator (bool 1)

Funktions-Aufruf-Operator (double 2.72, int 5)

Funktions-Aufruf-Operator (double 3.14, int 6)

Hinweis – der Funktions-Aufruf Operator muß immer als Element-Funktion implementiert

werden. Als freie Funktion kann er nicht implementiert werden.

13.7 Spezialitäten

Es lassen sich fast alle C++ Operatoren überladen – folgende sind die Ausnahme:

Komponentenzugriff „.“

Bereichszuordnung „::“

„sizeof“

Komponente über Komponentenzeiger „.*“

Bedingter Ausdruck „? :“

Cast-Operatoren: „static_cast“, „const_cast“, „reinterpret_cast“ und „dynamic_cast“

Das heißt, dass sich auch nicht so offensichtliche Operatoren überladen lassen, wie z.B.

Pfeil-Operator „->“

Komma-Operator „,“

Index-Operator „[ ]“

Hier noch ein paar Bemerkungen zu einigen speziellen Operatoren:

Für die Inkrement und Dekrement Operatoren „++“ und „—“ existieren jeweils 2 Varianten

für die Überladung, um die Prä- und die Postfix Notation zu unterscheiden.

Auch der Index Operator „[ ]“ läßt sich überladen – damit kann eine Klasse quasi Array-

Charakter bekommen – siehe z.B. „std::vector“ oder „std::string“.

Für die dynamische Speicherverwaltung stellt C++ die Operatoren „new“ und „delete“ in

verschiedenen Ausführungen zur Verfügung. Diese Operatoren können sowohl global als

auch klassenspezifisch überladen werden.

Neben Konvertierungs-Konstruktoren gibt es in C++ auch spezielle Konvertierungs-

Operatoren, die der Compiler für die implizite Typumwandlung nutzen kann (vergleichbar

zu den Konvertierungs-Konstruktoren).

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 158 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

13.8 Fazit

13.8.1 Grenzen

Die Operator-Überladung hat Grenzen:

Die Priorität, Syntax, Parameteranzahl und Auswertungsreihenfolge liegen fest.

Die Operatoren für nicht-benutzerdefinierte Datentypen können nicht verändert werden.

Es können keine neue Operatoren definiert werden, d.h. z.B. Operator „**“ zum

Potenzieren ist nicht möglich.

Operatoren dürfen keine Klassen-Funktionen („static“) sein – Ausnahmen „new“ und

„delete“.

Der Compiler macht keine eigenständigen Übertragungen. Aus „+“ und „=“ wird nicht

automatisch „+=“, bzw. die Postfix Semantik bei Inkrement und Dekrement Operatoren

wird nicht automatisch erzeugt.

Einige Operatoren können nicht überladen werden – s.o.

Fast alle Operatoren lassen sich als Element-Funktionen definieren (Ausnahme „new“

und „delete“) – aber nicht alle als freie Funktion:

Alle Zuweisungen wie z.B. =, *=, +=, …

Konvertierungen

( )

[ ]

->

new/delete

Bei Operator-Funktionen sind ausser beim Funktions-Aufruf-Operator keine Default-

Argumente zugelassen.

14 Vererbung & Polymorphie

14.1 Vererbung

Vererbung (genau genommen „öffentliche Vererbung“) ist die Modellierung einer "ist-ein"

Beziehung. Eine „ist-ein“ Beziehung meint, dass jedes Objekt der abgeleiteten Klasse ein

Objekt der Basis-Klasse ersetzen kann, ohne dass es sematische Probleme gibt.

Wenn A Basis-Klasse von B und C ist:

B ist abgeleitet von A => B ist ein A => alles was für A gilt, gilt auch für B

C ist abgeleitet von A => C ist ein A => alles was für A gilt, gilt auch für C

In Richtung der abgeleiteten Klassen findet eine Spezialisierung statt:

B ist eine Spezialisierung von A

Ein Pferd ist eine Spezialisierung eines Säugetiers

Alles, was für Säugetiere gilt (z. B. Alter, Gewicht, ... ), gilt auch für Pferde. All diese

Attribute und Funktionen erbt Pferd von Säugetier.

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 159 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

In Richtung der Basis-Klassen findet eine Generalisierung oder Verallgemeinerung

statt – Basis-Klassen fassen gemeinsame Dinge der abgeleiteten Klassen zusammen:

A enthält alle Gemeinsamkeiten von B und C.

Vogel enthält alles Vogel-typische, unabhängig, ob es sich um eine Amsel oder eine

Möve handelt.

Hinweise

Die abgeleitete Klassen (z. B. B und C) sind unabhängig voneinander.

Von einer Klasse können beliebig viele andere Klassen abgeleitet werden.

Eine Klasse kennt die von ihr abgeleiteten Klassen nicht.

Umgekehrt kennt die abgeleitete Klasse natürlich ihre Basis-Klasse.

Ein Basis-Klasse wird oft auch Super-Klasse genannt.

Eine abgeleitete Klasse wird oft auch Sub- oder Unter-Klasse genannt.

Achtung

Unterscheiden sie zwischen ”hat-ein” bzw. ”ist implementiert-mit” und ”ist-ein”.

Eine Person hat einen Namen, ist aber kein Name => Layering, Aggregation.

Eine Person ist ein Lebewesen, immer, ohne wenn und aber => Vererbung.

Hinweis – im Verlauf der Vorlesung ist mit Vererbung immer öffentliche Vererbung

gemeint - wenn nicht explizit anders gesagt. C++ untersützt auch noch nicht-öffentliche

Beziehung. Nicht-öffentliche Vererbung hat aber eine eine ganz andere Semantik, d.h. sie

modelliert keine „ist-ein“ Beziehung.

Hinweis – alle Vererbungen in diesem Kapitel sind Einfach-Vererbungen („single

inheritance“), d. h. eine Klasse hat genau eine Basis-Klasse. C++ unterstützt auch

Mehrfach-Vererbung („multiple inheritance“), die aus Zeitmangel leider nicht behandelt wird.

14.1.1 Implementation

Wie wird Vererbung in C++ implementiert?

Syntax

class Klassen-Name : [Vererbungs-Spezifizierer] Basis-Klasse { ... };

class A

{

public:

int ai;

void af();

};

class B : public A // Definition Klasse B oeffentlich abgeleitet von A

{

public:

int bi;

void bf();

};

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 160 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

int main()

{

A a;

B b;

a.ai=7; // okay

a.af(); // okay

a.bi=8; // Compiler-Fehler - A hat keine Varibale bi

a.bf(); // Compiler-Fehler - A hat keine Funktion bf()

b.ai=9; // okay - B hat Variable ai von A geerbt

b.af(); // okay - B hat Funktion af() von A geerbt

b.bi=10; // okay

b.bf(); // okay

}

A

B

Hinweis – die abgeleitete Klasse erbt die Funktionalität der Basis-Klasse, was man daran

sieht, dass das Objekt der abgeleiteten Klasse „B“ die Funktionen und Attribute von „A“

besitzt, ohne dass sie explizit programmiert werden mussten.

Die Vererbungshierarchie lässt sich beliebig fortsetzen, z.B. in dem man zusätzlich eine

Klasse „C“ von „B“ ableitet:

A

B

C

// Klasse A und B wie eben

class C : public B

{

public:

int ci;

void cf();

};

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 161 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

int main()

{

C c;

c.ai=11; // okay - C hat Variable ai von B (wiederum von A) geerbt

c.af(); // okay - C hat Funktion af() von B (wiederum von A) geerbt

c.bi=12; // okay - C hat Variable bi von B geerbt

c.bf(); // okay - C hat Funktion bf() von B geerbt

c.ci=13; // okay

c.cf(); // okay

}

Welche Elemente enthalten die einzelnen Klassen nun?

Klasse Attribut Element-Funktionen

A ai af()

B ai

bi

af()

bf()

C ai

bi

ci

af()

bf()

cf()

Denn

B ist ein A

C ist ein B

C ist ein A (Vererbung ist transitiv)

14.1.2 Konstruktoren

Konstruktoren müssen immer neu definiert werden, da sie defaultmäßig nicht vererbt

werden. Dies ist vernünftig, da ein ererbter Konstruktor nicht wissen kann, wie er die neuen

nicht-ererbten Attribute initialisieren soll.

Wird ein Objekt einer abgeleiteten Klasse erzeugt und ist für die Basis-Klasse kein spezieller

Konstruktor angegeben, so wird automatisch der Standard-Konstruktor der Basis-Klasse für

den Basis-Klassenanteil des Objekts genommen.

class A

{

public:

A() { cout << "Konstruktor A\n"; }

};

class B : public A // B abgeleitetet von A

{

public:

B() { cout << "Konstruktor B\n"; }

};

int main()

{

B b;

}

Ausgabe

Konstruktor A

Konstruktor B

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 162 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Hat die Basis-Klasse keinen Standard-Konstruktor, so muss der gewünschte Konstruktor in

der Initialisierungsliste der abgeleiteten Klasse angegeben werden.

class A

{

public:

A(int);

};

A::A(int i)

{

}

class B : public A // B abgeleitetet von A

{

public:

B();

B(int);

};

B::B() // Compiler-Fehler, kein Standard-Konstruktor

{

}

B::B(int i) // okay, explizite Angabe des Konstruktors

: A(i)

{

}

Beim Konstruieren eines Objekts wird vor den Konstruktoren der Attribute der Konstruktor

der Basis-Klasse aufgerufen.

Konstruktoren können vererbt werden. Dies wird mit einer Using-Deklaration gemacht.

class A

{

public:

A(int);

A(double);

};

class B : public A

{

public:

using A::A; // Erbt alle Konstruktoren von A

};

14.1.3 Destruktoren

Die Destruktoren werden umgekehrt abgearbeitet, d. h. von der abgeleiteten Klasse bis hin

zur Basis-Klasse. Es werden automatisch alle Destruktoren des Vererbungszweiges

durchlaufen.

class attribut

{

public:

attribut(int i) : n_(i) { cout << "attribut(" << n_ << ")\n"; }

~attribut() { cout << "~attribut(" << n_ << ")\n"; }

private:

int n_;

};

class base

{

public:

base() : att_(1) { cout << "base()\n"; };

~base() { cout << "~base()\n"; };

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 163 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

private:

attribut att_;

};

class derived : public base

{

public:

derived() : att_(2) { cout << "derived()\n"; };

~derived() { cout << "~derived()\n"; };

private:

attribut att_;

};

int main()

{

derived d;

}

Ausgabe

attribut(1)

base()

attribut(2)

derived()

~derived()

~attribut(2)

~base()

~attribut(1)

14.1.4 Qualifizierter Name

Manchmal ist es nötig, Symbole einer Klasse anzusprechen, die eigentlich nicht sichtbar

sind, da sie überschrieben oder verdeckt sind. In diesem Fall muß das Symbol über den

Namen der Klasse, den Scope-Resolution Operator und den eigentlichen Namen

referenziert werden.

class A

{

public:

void f();

};

class B : public A

{

public:

void g();

};

void B::g()

{

A::f(); // expliziter Aufruf der Element-Funktion f der Klasse A

}

int main()

{

B b;

b.g();

b.A::f(); // expliziter Aufruf der Element-Funktion f der Klasse A

}

Hinweis – ein mit Scope-Resolution Operator und Klassen-Name referenzierter Name

heisst qualifizierter Name.

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 164 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

14.1.5 Überschreiben von Funktionen

Eine abgeleitete Klasse erbt von der Basis-Klasse u.a. ihr Verhalten - die Funktionen. Das

ererbte Verhalten muss für die abgeleitete Klasse aber nicht korrekt sein. In manchen Fällen

ist es komplett falsch, in anderen stimmt das Prinzip, aber im Detail gibt es Abweichungen.

In solchen Fällen kann die ererbte Funktion von der abgeleiteten Klasse überschrieben

werden.

Achtung – das „Überschreiben“ dieses Kapitels ist nur halb fertig. Erst mit virtuellen

Funktionen wird Überschreiben voll funktionieren – siehe später.

class A

{

public:

void f() { cout << "A::f()\n"; }

};

class B : public A

{

public:

void f() { cout << "B::f()\n"; }

void g()

{

f(); // ruft B::f() auf

A::f(); // ruft A::f() auf

}

};

int main()

{

A a;

B b;

a.f(); // ruft A::f() auf

b.f(); // ruft B::f() auf

b.A::f(); // ruft A::f() auf

b.g();

}

Ausgabe

A::f()

B::f()

A::f()

B::f()

A::f()

Auf die Art und Weise kann eine nicht passende Implementierung einer Basis-Klasse in

einer abgeleiteten Klasse neu implementiert, d. h. überschrieben werden.

14.1.6 Zugriffsbereich protected

Zusätzlich zu den Zugriffsbereichen public und private gibt es Außerdem noch protected.

Der Zugriffsbereich protected liegt in seiner Wirkung zwischen public und private.

Im Gegensatz zu private kann auf Elemente im Zugriffsbereich protected auch noch von

abgeleiteten Klassen zugegriffen werden.

class A

{

public:

void fpublic();

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 165 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

protected:

void fprotected();

private:

void fprivate();

};

class B : public A

{

public:

void f();

};

void B::f()

{

fpublic(); // okay - Aufruf von A::fpublic()

fprotected(); // okay - Aufruf von A::fprotected()

fprivate(); // Compiler-Fehler - A::fprivate() ist nicht erreichbar

}

int main()

{

A a;

a.fpublic(); // okay - Aufruf von A::fpublic()

a.fprotected(); // Compiler-Fehler - A::fprotected() ist nicht erreichbar

a.fprivate(); // Compiler-Fehler - A::fprivate() ist nicht erreichbar

}

Achtung – beachten Sie bitte, dass bzgl. aller abgeleiteten Klassen der protected-Bereich

mit zur öffentlichen Schnittstelle, d. h. zum Interface gehört und entsprechend designt

werden sollte. Legen Sie d.h. auch in den protected-Bereich keine Datenelemente.

14.2 Konsequenzen aus der „ist-ein“ Beziehung

Ganz im Sinne der „ist-ein“ Semantik können in C++ Objekte einer öffentlich-abgeleiteten

Klasse auch immer für Objekte der Basis-Klasse stehen. Dies hat mehrere Konsequenzen.

14.2.1 Konsequenz 1

Wird in einem Ausdruck ein Objekt einer Basis-Klasse erwartet, so kann auch immer ein

abgeleitetes Objekt als Argument benutzt werden. Dies betrifft z.B. Funktions-Aufrufe oder

Zuweisungen, gilt aber für alle Arten von Ausdrücken.

class A { };

class B : public A { };

void f(A)

{

}

int main()

{

B b;

f(b); // okay - B Objekt wird als A benutzt, da B ein A ist

A a;

a = b; // okay - B Objekt wird als A benutzt, da B ein A ist

}

In diesem Beispiel wird „b“ in beiden Ausdrücken automatisch in ein A-Objekt gewandelt -

hierbei geht jede Information über den eigentlichen Typ verloren, d.h. in der Funktion „f“ ist

der Parameter wirklich ein A-Objekt, und auch das „a“ ist nur ein „A“ und mehr nicht.

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 166 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Achtung – das eine abgeleitete Klasse immer auch als Objekt der Basis-Klasse benutzt

werden kann, ist eine weitere fest in der Sprache verankerte Typ-Umwandlung, die z.B.

Einfluss auf die Aufruf-Regeln beim Funktions-Überladen hat.

Bemerkung – die Wandlung vom A-Objekt zum B-Objekt passiert beim Funktions-Aufruf,

indem der Compiler auf dem Stack Platz für ein A-Objekt reserviert und dann nur für den A

Anteil des B-Objekts den Kopier-Konstruktor aufruft. Achtung - hiermit verliert das B-Objekt

seine B-Identität und wird zu einem A-Objekt. Auch bei der Zuweisung wird nur der A-Anteil

des B-Objektes zugewiesen.

14.2.2 Konsequenz 2

Da ein Objekt einer abgeleiteten Klasse immer für ein Objekt einer Basis-Klasse stehen

kann, muss dies auch für jegliche Art von Referenzen auf Basis-Klassen-Objekte stimmen.

class A { };

class B : public A { };

int main()

{

B b;

A* p = &b; // okay - denn ein B "ist ein" A

A& r = b; // okay - denn ein B "ist ein" A

}

Auch dies ist ganz im Sinne der „ist-ein“ Semantik. Es ist ja auch korrekt, wenn sie z.B. auf

ein Auto zeigen und sagen: „Dies ist eine Maschine“, obwohl sie dabei eine sehr allgemeine

Abstraktion benutzen, aber ein Auto ist eben eine Maschine.

Hierbei verliert das Objekt auch nicht seine Identität, das es nicht kopiert, zugewiesen oder

sonstwie verändert wird. Es bleibt im Speicher ganz normal bestehen, und wird nur von

außen über verschiedene Typen referenziert.

14.2.3 Statischer und dynamischer Typ

Man unterscheidet in C++ den sogenannten statischen und den dynamischen Typ.

Der statische Typ ist der Typ, den der Compiler sieht, da er ohne wenn und aber zur

Compilezeit feststeht und eindeutig bekannt ist. Im Beispiel sind dies die Typ-Varianten

von “A“ der Variablen „p“ und „r“.

Dem gegenüber ist der dynamische Typ der echte Typ des Objekts, auf das verwiesen

wird. Dieser ist zur Compilezeit nicht zwingend bekannt, und muß nicht dem statischen

Typ entsprechen. Im Beispiel ist der dynamische Typ des referenzierten Objekts „B“,

obwohl der statische Typ „A“ ist.

class A { };

class B : public A { };

int main()

{

B b; // statischer Typ ist "B", dynamischer Typ auch

A* p = &b; // statischer Typ ist "A*", der dynamische Typ kann mehr sein...

A& r = b; // statischer Typ ist "A&", der dynamische Typ kann mehr sein...

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 167 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

int n; // statischer Typ ist "int", dynamischer Typ auch

int& ri = n; // statischer Typ ist "int&", dynamischer Typ auch

}

Der dynamische Typ kann sich nur dann vom statischen Typ unterscheiden, wenn:

die Variable ein Zeiger oder eine Referenz ist, und

der statische Typ eine Klasse ist, von der es Ableitung geben kann.

Manch einer bringt den Einwand, dass die Unterscheidung in statische und dynamische

Typen doch sinnlos ist – jeder sieht doch bei dem obigen Beispiel, dass „p“ und „r“ auf das B

Objekt „b“ verweisen. Bei dem obigen Beispiel ist dies richtig – es ist halt als einführendes

Beispiel sehr einfach. Schon bei einem nur etwas komplexeren Fall läßt sich der dynamische

Typ prinzipiell erst zur Laufzeit bestimmen – und hat damit seine Berechtigung.

class A { };

class B : public A { };

class C : public A { };

void fct(A& r) // Ginge auch mit Zeigern – genau der gleiche Effekt

{

... // Welchen Objekt-Typ referenziert "r" hier? Ein "A", "B" oder "C"?

}

int main()

{

A a;

B b;

C c;

fct(a);

fct(b);

fct(c);

}

14.3 Polymorphie

14.3.1 Bisheriges Verhalten

Bisher werden Element-Funktions-Aufrufe des Compilers über den statischen Typ einer

Variablen aufgelöst.

class A

{

public:

void f() const { cout << "A::f\n"; }

};

class B : public A

{

public:

void f() const { cout << "B::f\n"; }

};

class C : public B

{

public:

void f() const { cout << "C::f\n"; }

};

int main()

{

C c;

A& ra = c;

B& rb = c;

C& rc = c;

ra.f(); // => A::f

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 168 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

rb.f(); // => B::f

rc.f(); // => C::f

}

Ausgabe

A::f

B::f

C::f

Dieses Verhalten kommt daher, dass der Compiler die Funktions-Aufrufe zum Compile-

Zeitpunkt fest verdrahtet (statische Bindung, frühe Bindung, static binding). Hierbei wird nicht

der echte dynamische Objekt-Typ benutzt, da der Compiler diesen nicht wissen kann. So

bindet der Compiler den Funktions-Aufruf fest über den statischen Typ, über den den der

Funktions-Aufruf vorgenommen wird. Diese direkte Verdrahtung erklärt ja auch die

Performance von Funktions-Aufrufen in C++.

Anders ist dies bei virtuellen Funktionen.

14.3.2 Virtuelle Funktionen

Bei virtuellen Funktionen wird der Funktions-Aufruf erst zur Laufzeit festgelegt (dynamische

Bindung, späte Bindung, late binding). Hierbei wird zur Laufzeit quasi der echte dynamische

Typ des Objekts bestimmt und die Funktion dieses Typs aufgerufen.

Um eine Element-Funktion zu einer virtuellen Funktion zu machen, muss das Schlüsselwort

„virtual“ vor die Element-Funktions-Deklaration geschrieben werden – hierbei reicht die

unterste Ebene. Bei der Definition einer virtuellen Funktion darf das Schlüsselwort virtual

nicht auftauchen.

Die überschriebenen Funktionen in den abgeleiteten Klassen werden typischerweise ohne

„virtual“ definiert (die Angabe ist aber erlaubt). Stattdessen werden sie mit dem

Schlüsselwort „override“ ausgezeichnet (darf aber auch weggelassen werden). „Override“

sorgt dafür, dass der Compiler meckert, wenn Sie entgegen der Aussage keine Funktion

überschreiben.

Syntax (Element-Funktions-Deklaration)

virtual rueckgabetyp funktionsname ( parameterliste );

virtual rueckgabetyp funktionsname ( parameterliste ) const;

class A

{

public:

virtual void f() const { cout << "A\n"; }

};

class B : public A

{

public:

void f() const override { cout << "B\n"; }

};

class C : public A

{

public:

void f() const override { cout << "C\n"; }

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 169 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

};

class D : public C

{

public:

void f() const override;

};

void D::f() const

{

cout << "D\n";

}

class E : public A { };

class F : public E

{

public:

void f() const override { cout << "F\n"; }

};

int main()

{

A a;

B b;

C c;

D d;

E e;

F f;

A* p;

p=&a;

p->f(); // p zeigt auf ein A-Objekt => A::f() -> Ausgabe A

p=&b;

p->f(); // p zeigt auf ein B-Objekt => B::f() -> Ausgabe B

p=&c;

p->f(); // p zeigt auf ein C-Objekt => C::f() -> Ausgabe C

p=&d;

p->f(); // p zeigt auf ein D-Objekt => D::f() -> Ausgabe D

p=&e;

p->f(); // p zeigt auf ein E-Objekt => A::f() -> Ausgabe A

// da E keine eigene Fkt. f() hat

p=&f;

p->f(); // p zeigt auf ein F-Objekt => F::f() -> Ausgabe F

}

Ausgabe

A

B

C

D

A

F

Eine Funktion ist ab der Klasse in der Vererbungs-Hierarchie virtuell, wo sie in der

Klassendefinition zum ersten Mal mit virtual deklariert wurde. Wird in den weiteren

abgeleiteten Klassen die virtual Deklaration weggelassen, so ist die Funktion trotzdem

weiterhin virtuell.

Achtung – nur Element-Funktionen können virtuell sein und überschrieben werden. Weder

Klassen-Funktionen noch freie Funktionen können virtuell sein, da sie keinen Objekt-Bezug

haben, über den die Funktions-Auswahl („Funktions-Dispatch“) zur Laufzeit möglich ist.

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 170 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

14.3.3 Diskussion

Mit Polymorphie ist gemeint, dass eine Funktion vielgestaltig ist, d. h. in Abhängigkeit vom

Kontext unterschiedlich (angepasst) reagiert. Genau genommen reagiert natürlich nicht eine

Funktion unterschiedlich, sondern es werden unterschiedliche Funktionen aufgerufen, ohne

dass sich der Entwickler um die echten Objekt-Typen und deren verschiedene Funktionen-

Implementierungen kümmern muss. Dies ermöglicht es ihm, ähnliche Objekte gleich zu

behandeln, ohne Details kennen zu müssen (z.B. welche Klassen es gibt, wie sie heissen,

wie sie zu behandeln sind, usw...).

Hinweis – im ersten Augenblick sieht Polymorphie nicht nach was Besonderem aus,

sondern eher nur nach einem kleinen Sprachgag – aber dies ist falsch. Es ist das

Schlüsselkonzept der Objektorientierung. Seine wahre Mächtigkeit erkennt man meist erst

in praktischen Einsätzen.

14.4 Destruktoren

Destruktoren sind normale Element-Funktionen:

class A

{

public:

~A() { cout << "- De. A\n"; }

};

class B : public A

{

public:

~B() { cout << "- De. B\n"; }

};

int main()

{

cout << "Zeiger vom Typ B*\n";

B* pb = new B();

delete pb;

cout << "Zeiger vom Typ A*\n";

A* pa = new B();

delete pa; // Achtung - undefiniertes Verhalten

}

Ausgabe

Zeiger vom Typ B*

- De. B

- De. A

Zeiger vom Typ A*

- De. A

Wie man an der Ausgabe des Beispiels sieht, wird daher bei Objekten die über Basis-

Klassen-Zeiger gelöscht werden, nur der Destruktor der Basisklasse aufgerufen. Ein

Destruktor ist halt eine normale Element-Funktion, und wird daher defaultmäßig statisch

gebunden, d.h. der Destruktor-Aufruf wird über den statischen Typ festgelegt.

Lösung

Die Lösung besteht natürlich darin, den Destruktor virtuell zu machen. Da der implizite

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 171 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

Destruktor nicht virtuell ist, müssen wir in diesem Fall immer selber einen erzeugen - und sei

er auch leer. Im einfachsten Fall wird in die Basis-Klasse ein leerer virtueller Destruktor

eingefügt.

class A

{

public:

virtual ~A() {}

};

class B : public A

{

public:

virtual ~B() { cout << "- De. B\n"; }

};

int main()

{

cout << "Zeiger vom Typ B*\n";

B* pb = new B();

delete pb;

cout << "Zeiger vom Typ A*\n";

A* pa = new B();

delete pa;

}

Ausgabe

Zeiger vom Typ B*

- De. B

Zeiger vom Typ A*

- De. B

Regeln

Da Sie nie wissen können, wie Ihre Klasse mal benutzt wird, sollten Sie sich nur von

Klassen ableiten, die einen virtuellen bzw. protected Destruktor haben.

Umgekehrt sollten Sie immer, wenn Sie eine Basis-Klasse entwickeln, einen virtuellen

Destruktor implementieren – auch wenn dieser leer ist.

Sie sollten jetzt nicht übertreiben und bloß nicht jeder Klasse einen virtuellen Destruktor

geben. Sobald eine Klasse mindestens eine virtuelle Funktion hat, wird sie größer, ihre

Benutzung wird unter Umständen langsamer, und falls sie vorher ein POD war so würde

sich das nun ändern.

14.5 Abstrakte Basis-Klassen

Basis-Klassen sind häufig allgemeine Konzepte wie z.B. Maschine, Fahrzeug oder Widget.

Es gibt aber kein allgemeines Fahrzeug, sondern nur zum Beispiel konkrete Autos. Darum

sollten sich von solchen Klassen eigentlich keine Objekte erzeugen lassen, da sie keinen

Sinn machen. Außerdem hat man häufig das Problem, dass man nicht weiß, wie man

Funktionen in der Basis-Klasse implementieren soll.

C++ hat ein Sprachmittel für beide Probleme: Abstrakte Klassen mit rein virtuellen

Funktionen

Abstrakte Klassen sind Klassen, von denen keine Objekte erzeugt werden können. Eine

Klasse ist dann abstrakt, wenn sie mindestens eine rein-virtuelle Funktion besitzt (auch

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 172 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

durch Vererbung).

Rein-virtuelle Funktionen sind normale virtuelle Funktionen, die in der Basis-Ebene (im

Normallfall) keine Implementierung haben und zwingend in den abgeleiteten Klassen

überschrieben werden müssen. Deklariert werden sie in der KlassenDefinition mit einem = 0.

class A

{

public:

virtual void f() = 0;

};

A a; // Compiler-Fehler - von abstrakten Klassen kann kein Objekt erzeugt werden

class A

{

public:

virtual void f() = 0;

};

class B : public A { };

class C : public B

{

public:

void f() override {}

};

A a; // Compiler-Fehler, da abstrakte Klasse

B b; // Compiler-Fehler, da abstrakte Klasse (durch Vererbung)

C c; // okay

Was für einen Sinn haben abstrakte Klassen?

Sie verhindern, dass von diesen Klassen Objekte gebildet werden.

Sie erzwingen, dass bestimmte Funktionen (die rein-virtuellen) überschrieben werden.

Außerdem stellen sie ein allgemeines Interface für verschiedene Implementierungen dar.

Man bezeichnet Basis-Klassen, vor allem abstrakte Basis-Klassen – im Extrem solche mit

nur rein-virtuellen-Funktionen – gerne als Interface. Sie stellen eine allgemeine Sicht auf

eine Abstraktion da, und bilden quasi die Schnittstelle zu den konkreten Klassen.

14.6 Dynamic-Cast

In sehr seltenen Fällen ist es nötig, einen Objekt-Zeiger oder eine Objekt-Referenz in der

Klassen-Hierarchie hinauf zu den abgeleiteten Klassen zu casten.

Dieser Cast ist – wie eigentlich alle Casts – nicht unproblematisch. Hier ist der Cast aber

besonders problematisch, denn es ist zur Compilezeit oft nicht bestimmbar, ob sich der

Quell-Ausdruck überhaupt in den Ziel-Typ umwandeln läßt.

class A

{

public:

virtual ~A() {}

virtual void f();

};

class B : public A {};

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 173 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

class C : public A

{

public:

void g();

};

void fct(A* pa)

{

pa->g(); // Wie kann ich daraus einen C-Zeiger machen?

// Und was ist, wenn es kein Objekt vom Typ C ist, sondern z.B. ein B?

}

Mit dynamic_cast existiert in C++ ein Cast für Klassen-Hierarchien. Für ihn gilt:

Quell- und Ziel-Typ müssen in einer gemeinsamen Klassen-Hierarchie liegen, oder der

Zieltyp muss ‘void*’ sein. Mit Quell-Typ ist der statische Typ des Quell-Ausdrucks

gemeint.

Der Quell-Typ muss polymorph sein, d.h. er muss mindestens eine virtuelle Funktion

enthalten.

Der Ziel-Typ muss nicht polymorph sein.

Ein Dynamic-Cast kann nur auf Zeiger und Referenz-Typen ausgeführt werden.

Der Dynamic-Cast wird nur ausgeführt, wenn der Quell-Ausdruck dem Ziel-Typ entspricht.

Die Konvertierung findet zur Laufzeit statt - sie ist daher langsamer als andere Casts.

Ein Dynamic-Cast geht korrekt mit virtuellen Adressen bei Mehrfach-Vererbung um.

Ein Dynamic-Cast kann keinen const Modifizierer entfernen.

Wird ein nicht korrekter Cast versucht, so wird:

bei Zeigern ein Null-Zeiger zurückgegeben, und

bei Referenzen eine std::bad_cast Exception geworfen.

// Klassen A, B und C wie oben

void f1(A* pa)

{

cout << "> f1\n";

if (C* pc = dynamic_cast<C*>(pa))

{

pc->g();

}

else

{

cout << " Null-Zeiger\n";

}

}

void f2(A& ra)

{

cout << "> f2\n";

try

{

dynamic_cast<C&>(ra).g();

}

catch (const std::bad_cast& x)

{

cout << " Exception: " << x.what() << '\n';

}

}

int main()

{

A a;

cout << "A\n";

f1(&a);

f2(a);

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 174 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

B b;

cout << "\nB\n";

f1(&b);

f2(b);

C c;

cout << "\nC\n";

f1(&c);

f2(c);

}

Ausgabe

A

> f1

Null-Zeiger

> f2

Exception: Bad dynamic_cast!

B

> f1

Null-Zeiger

> f2

Exception: Bad dynamic_cast!

C

> f1

C::g()

> f2

C::g()

Bemerkung – auch mit den Sicherheiten von „dynamic_cast“ ist ein Cast ein Cast und bleibt

ein Cast. Auch wenn er relativ schön und sicher ist. Cast’s sind und bleiben ein Werkzeug

für absolute Ausnahme-Situationen. Im Normallfall benötigen sie bei einem ‘vernünftigen’

Design keine Cast’s, auch kein „dynamic_cast“.

14.7 Vererbung & Polymorphie

14.7.1 Semantik

Über die Semantik dieser neuen Sprachmittel lassen sich folgende Faustregeln aufstellen:

Eine gemeinsame Basis-Klasse bedeutet gemeinsame Aufgaben.

Öffentliche Erblichkeit bedeutet "ist-ein".

Eine rein-virtuelle Funktion bedeutet, dass die Schnittstelle der Funktion geerbt wird.

Eine virtuelle Funktion bedeutet, dass die Schnittstelle und eine Standardimplementierung

geerbt werden.

Eine nicht-virtuelle Funktion bedeutet, daß die Schnittstelle inkl. obligatorischer

Funktionen geerbt wird.

Oberbegriffe (Basis-Klassen) sind ein Hilfsmittel zur Abstraktion und bilden ein

gemeinsames Interface aller abgeleiteten Klassen.

Polymorphie bedeutet, daß eine Funktion, je nach Objekt, angepasst reagiert.

14.7.2 Begriff „Polymorphie“

Mit Vererbung und virtuellen Funktionen wird in C++ Polymorphie (Vielgestaltigkeit)

Folien für die C++ Vorlesung FH Aachen WS 2019/20 - Version 6 Seite 175 / 175

© Detlef Wilkening 2020 www.wilkening-online.de

realisiert. Mit Polymorphie ist gemeint, dass eine Funktion vielgestaltig ist, d. h. in

Abhängigkeit vom Kontext unterschiedlich (angepasst) reagiert. Genau genommen reagiert

nicht eine Funktion unterschiedlich, sondern es werden unterschiedliche Funktionen

aufgerufen.

Der Begriff Polymorphie wird in der Literatur sehr unterschiedlich benutzt:

1. Einige bezeichnen schon jeden Aufruf einer Element-Funktion als Polymorphie, da jede

Klasse die gleichen Funktions-Namen enthalten kann, und daher in Abhängigkeit vom

Objektbezug unterschiedliche Funktionen aufgerufen werden.

2. Manche bezeichnen Überladen als Polymorphie, da hier unterschiedliche Funktionen in

Abhängigkeit von den Parametern aufgerufen werden.

3. Ich beschränke mich hier bei dem Begriff Polymorphie (im Einklang mit dem Grossteil der

OO Literatur) auf die Wirkungsweise von dynamisch gebundenen (in C++ also virtuellen)

Funktionen. Manchmal wird dies in C++ auch dynamische Polymorphie genannt.

14.7.3 Schlüsselkonzepte

Mit Vererbung und vor allem Polymorphie haben wir die Schlüsselkonzepte der

objektorientierten Programmierung kennengelernt. Immer, wenn sie eine Menge an

ähnlichen Dingen, verwalten, bearbeiten, oder sonstwas müssen, bietet sich Polymorphie als

eine elegante Lösung an.

Ob Sie nun

ein Spiel mit unterschiedlichen Spielern entwickeln, die auf unterschiedlichen Planeten

leben, in dem unterschiedliche Raumschiffe mit unterschiedlichen Waffen vorkommen,...

oder ein Programm zur Kontoführung unterschiedlicher Konten,

oder, oder, oder....

Immer bieten sich Vererbung und Polymorphie als einfache, elegante und leistungsfähige

Designmittel an. Daher finden sie sich auch immer wieder als zentrale Konzepte in Pattern,

Frameworks, Bibliotheken und Programmen wieder.

Wenn Sie es schaffen, dass eine Programm-Struktur (die Architektur) nur auf abstrakten

Klassen beruht, können Sie die konkreten Implementierungen verändern, ersetzen, löschen,

ergänzen und umstrukturieren, ohne dass Sie die Programm-Struktur nur ein einziges Mal

ändern müssen. Damit hätten Sie einen sehr hohen Grad an Erweiterbarkeit und

Änderbarkeit erreicht.