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.