Sie sind auf Seite 1von 615

Programmieren in C++

Bernhard Seeger
Philipps Universität Marburg
Fachbereich Mathematik und Informatik

1
Übersicht
 Einführung und Geschichte
 Programmiersprache C
 Grundlagen der Programmiersprache C
 Ausdrücke, Zuweisungen
 Anweisungen: bedingte Anweisungen; Schleifen
 Datenstrukturen
 Arrays; Pointer; Structs
 Unterprogramme, Funktionen, Rekursion
 Modularisierung, Header Dateien
 JNI: Anbindung von C-Programme in Java
 Programmiersprache C++
 Klassenkonzept, Vererbung, …
 C++-Bibliotheken: STL

2
Literatur – Die Klassiker

 Brian W. Kernighan, Dennis M. Ritchie:


Programmieren in C. ANSI C (2. A.). Mit dem C- Reference Manual.
Taschenbuch Carl Hanser Verlag; ISBN: 3446154973
 Brian W. Kernighan, Dennis M. Ritchie:
The C Programming Language
Taschenbuch - 274 Seiten - 2nd (Mai 1988), Prentice Hall; ISBN:
0131103628
 Stroustrup, Bjarne :
Die C++ Programmiersprache
Taschenbuch - 1000 Seiten - 4., aktualis. u. erw. Aufl. (2000)
Addison-Wesley; ISBN: 3827317568
 Stroustrup, Bjarne :
The Design and Evolution of C++
Taschenbuch - 352 Seiten - Reissue (1994)
Addison Wesley Longman Publishing Co; ISBN: 0201543303

3
Warum C und C++?

 C und C++ zählen derzeit zu den am meisten benutzten


Programmiersprachen!

4
Geschichte von C

 Vorgeschichte von C
 C
 Von C zu C++
 Bjarne Stroustrup: Zitate und Meinungen zu C

5
Vorgeschichte von C

• C wurde Ende der 60er Jahre primär für die erste


Implementierung des UNIX Betriebsystems entwickelt. Die
unmittelbaren "Vorgänger" von C waren die Sprache BCPL
und B (Ken Thompson).

• Andere Einflüsse stammen angeblich auch von


• ALGOL 60 und ALGOL 68
• PL/360 und PL/M

6
Ziele beim Sprachentwurf

 Kleiner Sprachumfang
 Beschränkung auf die wichtigsten Kontrollstrukturen und
Datentypen
 Einfache Compiler, die C-Programme schnell übersetzen
können
 schlanke Laufzeitumgebung
 Abstraktion von Hardware und Betriebsystem
 Unterstützung der Sprache auf verschiedenen Plattformen
 Erstellen schneller Programme
 Direkter Zugriff auf Hardware wie z. B. Speicher

7
Entwicklung von C

 Entwickelt von Dennis Ritchie bei den AT&T Bell


Laboratorien
 Implementierungssprache von UNIX
 1989 Standardisierung als ANSI C (oder C89)
 1999 zweiter C-Standard: C99
 In dieser Vorlesung werden wir kurz auf die wenigen Änderungen in
C99 eingehen, aber uns im Wesentlichen auf C89 fokussieren.

8
Zitate von Bjarne Stroustrup (1)

 Fragen:
 "Why use C? Why didn't you build on, say, Pascal?“
 "C is clearly not the cleanest language ever designed nor the easiest
to use so why do so many people use it ?“
 Antworten
 "C is flexible: It is possible to apply C to most every application area
and to use most every programming technique with C. The language
has no inherent limitations that preclude particular kinds of programs
from being written".
 "C is efficient: The semantics of C are «low level»; that is, the
fundamental concepts of C mirror the fundamental concepts of a
traditional computer. Consequently, it is relatively easy for a compiler
and/or programmer to efficiently utilize hardware resources for C
programs".

9
Zitate von Bjarne Stroustrup (2)

 Antworten
 "C is available: Given a computer, whether the tiniest micro or the
largest super-computer, chances are that there is an acceptable
quality C compiler available and that that C compiler supports an
acceptably complete and standard C language and library.
Libraries and support tools are also available, so that a programmer
rarely needs to design a new system from scratch".
 "C is portable: A C program is not automatically portable from one
machine (and operating system) to another, nor is such a port
necessarily easy to do. It is however, usually possible and the level
of difficulty is such that porting even major pieces of software with
inherent machine dependences is typically technically and
economically feasible".

10
1. Einfache C-Programme

 Ein möglichst kurzes C Programm


 "Hallo Welt" im C Stil
 Übersetzung und Ausführung mit Visual C++
 Der Präprozessor
 Kommentare

11
Ein sehr kurzes C-Programm

void main ( ) { }

Die Argumente von Der leere Prozedurrumpf


main. von main.

 Das Programm definiert eine argumentlose Funktion namens


"main". Jedes ausführbare C Programm muss eine Funktion
"main" enthalten.
 Zwischen Funktionen und Prozeduren wird nicht
unterschieden.
 Bei Prozeduren kann man die Definition des Ergebnistyps und die
Rückgabe eines Funktionswertes weglassen.

12
Noch ein kurzes C89-Programm

int main ( ) { return 1; }

 Dieses Programm definiert "main" als Funktion mit einem


Ergebnis vom Typ "int" (Integer).
 Zusätzlich enthält der Rumpf eine Anweisung: "return 1" .
 Diese veranlasst die Rückgabe des Wertes 1 als
Funktionsergebnis
 in diesem Fall an das "main" aufrufende Betriebssystem.
 Beim Weglassen von "return 1" wird ein zufälliger Wert
zurückgegeben.

Achtung:
 Jede Anweisung muss mit einem Semikolon abgeschlossen
werden !

13
Noch ein kurzes C99-Programm

int main (void) { return 1; }

 Im Unterschied zu C89 muss in C99 eine parameterlose


Funktion durch die leere Parameterliste (void)
gekennzeichnet werden.

14
Ein sinnvolles Programm

Die stdio- Bibliothek soll benutzt


#include <stdio.h> werden. Zu dem Zweck werden
int main(void) { die verfügbaren Definitionen aus
printf("Hallo Welt!\n"); der Datei stdio.h eingelesen.
return 0;
}
Eine Zeichenkette wird mit der
Funktion printf ausgegeben.

 Das Programm gibt den Text "Hallo Welt !" aus.


 Allgemein kann mit printf auf diese Weise eine
Zeichenkette (String) ausgegeben werden.
 Ein String-Literal ist in " ... " eingeschlossen und enthält keinen
Zeilenumbruch.
 Eine Reihe von "Sonderzeichen" kann mit der Methode \x
ausgegeben werden. \n steht für "newline", \b für "backspace", \\ für
"\", \" für """, usw.

15
Übersetzen, Linken und Ausführen

Traditionelle Art
 Den Programmtext mit einem Texteditor eingeben und in
einer Datei mit dem Suffix "c" abspeichern.
 Übersetzen (und Binden) des Programms z. B. mit dem gcc-
Compiler
 gcc Hallo.c -o Hallo
 Dadurch wird ein ausführbares Programm mit dem Namen Hallo.exe
(unter Windows) erzeugt.
 Ausführen des Programms

Moderne Art
 Unter Verwendung einer integrierten
Entwicklungsumgebung wie z. B. Eclipse, Visual Studio, …

16
Eine Alternative bei Fensterbenutzung

Problem
 Wird das Programm in einer Entwicklungsumgebung unter
Windows übersetzt, sieht man nicht die Ausgabe, weil das
Fenster sofort geschlossen wird.

Lösung
 Vor der Zeile return 0 sollte man dann immer den Befehl
getchar(); einfügen.
 Damit wartet das Programm, bis die ENTER-Taste betätigt wurde,
bevor es sich beendet.

17
Präprozessor

 Bevor ein C Programm übersetzt wird, startet ein


Präprozessor. Dieser manipuliert das Programm, bevor
der Compiler es zu sehen bekommt.
 Zwei von (mehreren) Aufgaben des Präprozessors:
 Makrosubstitution: #define ....
 Einfügen von Header (und anderen) Dateien: #include ...

 #include "dateiname" sucht nach Benutzerdateien in dem


aktuellen Verzeichnis des Benutzers.
 #include <dateiname> sucht nach Standarddateien in
einem für solche Dateien definierten Verzeichnis.

18
Binder

 Im Gegensatz zu Java wird in C zwischen dem Kompilieren


und dem Binden eines Programms unterschieden.
 Ein Programm besteht aus verschiedenen, separat
voneinander übersetzten Dateien (Übersetzungseinheiten).
 Eine Übersetzungseinheit ist unabhängig von der konkreten
Programmiersprache!
 Durch Übersetzen entstehen dann Objektdateien.
 Linux: Objektdateien sind mit dem Suffix o gekennzeichnet.
 MS: Objektdateien sind mit dem Suff obj gekennzeichnet.
 Der Binder (Linker) hat die Aufgabe, die verschiedenen Teile
zu einer ausführbaren Einheit zu verschmelzen.
 Statisches Linken
 Dynamisches Linken

19
Kommentare

 Um die Lesbarkeit eines Programms zu erhöhen, verwendet man


Kommentare. Dazu gibt es zwei Möglichkeiten.
 Ein geklammerter Kommentar, der sich über viele Zeilen erstrecken
kann. Er kann z.B. benutzt werden, um Programmteile
herauszukommentieren.
 Kommentaranfang: /*
 Kommentarende: */
 In C99 können auch die aus Java bekannten Zeilenkommentare
benutzt werden. Diese beginnen mit dem Kommentarzeichen und
erstrecken sich bis zum Ende der laufenden Zeile
 Kommentarzeichen: //

20
Beispiel
#include <stdio.h>
#define Max 15 // Präprozessor ersetzt Max durch 15

int main (void) {


int k,i;
char x,arr[Max+1] = {"SORTIERBEISPIEL"};
/* Das Array arr besteht aus 16 einzelnen Feldern (0-15)
 wegen der Null am Stringende */

printf("%s\n",arr); ;
for (k = 1; k < Max; k++){
if (arr[k] < arr[k-1]) {
x = arr[k];
for (i = k; ( (i > 0) && (arr[i-1] > x) ); i--)
arr[i] = arr[i-1];
arr[i] = x;
}
}
printf("%s\n",arr);
21
}
2. Lexikalische Symbole

Fünf Arten von lexikalischen Symbolen


 Bezeichner:
 Beliebig lange Folge von Buchstaben oder Ziffern
 Unterstrich gilt als Buchstabe
 Das erste Zeichen muss ein Buchstabe sein
 Groß- und Kleinbuchstaben werden unterschieden
 Schlüsselworte: (siehe nächste Folien)
 besondere Bedeutung in C
 dürfen nicht als Bezeichner verwendet werden
 Literale:
 konstante Werte
 Operatoren (und andere Trennzeichen)
 "spezielle Symbole " wie z. B. +,-,…
 Leerzeichen, Tabs, Kommentare, Zeilenvorschübe
 "Whitespace-Zeichen": werden vom Compiler ignoriert

22
Schlüsselwörter von C89

auto double int struct


break else long switch
case enum register typedef
char extern return union
const float short unsigned
continue for signed void
default goto sizeof volatile
do if static while

Weitere Schlüsselwörter von C99


_Bool _Complex _Imaginary inline
restrict

23
Operatoren und andere Trennzeichen

Spezielle Symbole, die aus einem Zeichen bestehen:


! % ^ & ( ) - + = { } | ~ [ ] \ ; ' : " < > ? , . /

Spezialzeichen, die aus zwei bzw. drei Zeichen bestehen:


-> ++ -- .* ->* << >> <= >= == != &&
|| *= /= %= += -= <<= >>= &= ^= |= ::

Zusätzlich werden vom Präprozessor verwendet:

# ##

24
Einfache Datentypen von C

Boolean: bool Diesen Typ gibt es nur in C99!

Zeichen: char wchar_t

Ganze Zahlen: int

Fließzahlen: float double

Ohne Wert: void

25
Typmodifikatoren

 Vorzeichen
 signed mit Vorzeichen
 unsigned ohne Vorzeichen
 Länge
 short Speicher: halbe Wortlänge (2 Byte)
 long Speicher: ganze, doppelte oder dreifache
Wortlänge
 Anwendung auf ganze Zahlen
 long und short sind Synonyme für long int und short int
 long long ist typischerweise eine ganze Zahl aus 8 Byte
 unsigned char sind die Zahlen von 0,1,…,255
 Anwendung auf Fließkommazahlen
 long double
 Der reservierte Speicherbereich und damit auch der
Zahlenbereich für einen einfachen Typ in C ist
maschinenabhängig!

26
Größe eines Datentyps

 In C gibt es den Operator sizeof, mit dem man die Größe in Bytes
eines beliebigen Datentyps herausfinden kann.
 sizeof(Typname)
 Beispiel: sizeof(int)

Folgende Gesetze gelten in C


sizeof(char) == 1 und sizeof(short int) == 2
sizeof(short int) <= sizeof(int) <= sizeof(long int)
sizeof(float) <= sizeof(double) <= sizeof(long double)

27
Literale

Literale dienen zur Darstellung von Konstanten.

Literale repräsentieren
 Integer-Konstanten
 Character-Konstanten
 Floating-Point-Konstanten (Real-Konstanten)
 String-Literale (konstante Zeichenketten)

28
Ganzzahlige Literale

 Integer-Konstanten bestehen üblicherweise aus einer Folge


von Dezimalziffern.
 Eine Ziffernfolge, die mit 0 beginnt, wird als Oktalzahl interpretiert.
 Eine Ziffernfolge, die mit 0X beginnt, wird als Hexadezimalzahl
interpretiert.
 Beispiel: 255, 0377 und 0XFF repräsentieren die gleiche Zahl.
 Der Typ einer Konstanten kann durch Anhängen eines Suffix
gesetzt werden.
 255 ist eine Konstante, die automatisch den Typ int erhält
 255u erhält den Typ unsigned int
 255l erhält den Typ long int
 255ul erhält den Typ unsigned long int

29
Character- und String-Literale

 Character-Konstanten werden in " ' " eingeschlossen.


 String-Literale werden in " " " eingeschlossen.
 Folgende Ausnahme-Regeln gelten für spezielle Zeichen:
newline NL (LF) \n
octal number 000 \000
hex number hhh \xhhhh
horizontal tab HT \t
vertical tab VT \v
backspace BS \b
carriage return CR \r
form feed FF \f
alert BEL \a
backslash \ \\
question mark ? \?
single quote ' \'
double quote " \"

30
Beispiele

 'a' ist eine Character Konstante mit Wert: a


 '\?' ist eine Character Konstante mit Wert: ?
 '\'' ist eine Character Konstante mit Wert: '
 '\x4B' ist eine Character Konstante mit Wert: k

 Mehrere String-Literale werden automatisch aneinandergehängt


und mit einem abschließenden Null-Zeichen terminiert.
"Text1" "Text2" "Text3" ist ein String mit 16 Zeichen

31
Fließpunktzahlen

Fließpunktliterale werden in C/C++ immer durch einen


Dezimalpunkt ausgezeichnet:
123.456 und 123.456E+78
sind Fließpunktzahl-Konstanten.

Ohne zusätzliches Suffix haben Konstanten den Typ double.


Die Suffixe
 f bzw. F erzwingen den Typ float,
 l bzw. L erzwingen long double.
123., 123.456, 123.456e78 double-Konstanten
123.F, 123.456F, 123.456e78F  float-Konstanten
123.L, 123.456L, 123.456e78L  long double-Konstanten
123F ist kein gültiges float-Literal (Dezimalpunkt fehlt)!

32
Bezeichner

 Bezeichner werden in C wie in Java verwendet, um Konstanten,


Variablen, Funktionen (und anderes) zu benennen.
 Bezeichner haben einen Gültigkeitsbereich.
 globalen Gültigkeitsbereich
Dieser bezieht sich auf die komplette Übersetzungseinheit (c-Datei).
Ein Bezeichner gilt in diesem Bereich von der Stelle an, an der er
definiert ist.
 lokalen Gültigkeitsbereich
Diese entstehen wie in Java durch { } Klammerung.

{
{
{ n gilt nur in diesem
int n; Bereich - genauer:
}
} von der Definitionsstelle an.
}
33
Konventionen

 Konventionen bei der Auswahl der Bezeichner erleichtern die


Lesbarkeit eines Programms
 Namen der Bezeichner sollen die Bedeutung der
Variablen/Konstanten illustrieren.

 Typische Konventionen
 Variablennamen beginnen mit einem kleinen Buchstaben.
 eins
 laufIndex
 Konstantennamen bestehen nur aus großen Buchstaben
 MAX
 MIN
 PI

34
Casts

 Unter einem Cast versteht man die Umwandlung von Werten eines
Datentyps in Werte eines anderen Datentyps.
 Z. B. kann ein int-Wert in einen anderen double-Wert
umgewandelt werden und umgekehrt.
 Eine Typumwandlung kann implizit vom Compiler veranlasst werden
oder explizit durch einen Cast-Ausdruck erzwungen werden.

int i = 3, j, k;
double x = 4.5, y, z;
y = i + x; // a) int + doubledouble
j = i + x; // b) int + doubledoubleint
z = (double) i + x; // expliziter Cast
k = i + (int) x; // besser: expliziter Cast

 Fall a: i wird implizit vor der Addition nach double konvertiert.


 Fall b: in C grundsätzlich erlaubt, sollte aber vermieden werden.
35
Beispiele impliziter Casts

int main(void) {
int i = 3, j = 4, k;
double x=2.5, y=-3.14159, z;
unsigned int a = 5, b = 3, c;

k = i + j; // 3 + 4 = 7 ==> k = 7;
z = x + y; // 2.5 + -3.14159 = -0.61459
// ==> z = -0.61459
k = i + x; // 3.0 + 2.5 = 5.5 ==> k = 5
z = i + x; // 3.0 + 2.5 = 5.5 ==> z = 5.5
k = -2 + 0.5; // -2.0 + 0.5 = -1.5 ==> k = -1

c = i + j; // 3 + 4 = 7 ==> c = 7
c = i - j; // 3 - 4 = -1 ==> c = 65535 ?
// (je nach Bitgröße eines int)
}

36
Noch ein Beispiel

int main(void){
int i = 7, j = 3, k;
double x;
k = i / j; // 7/3 = 2 ==> k = 2
x = i / j; // 7/3 = 2 ==> x = 2.0 !!!
x = (double) i/j; // x = 2.333333
}

 Zuerst wird der Ausdruck auf der rechten Seite ausgewertet


und dann erst folgt die implizite Typanpassung!

37
Implizite Casts mit Typ char

int main(void){
char a = 'a', b = 'h', c;
int i;
c = a + 1; // c = 'b'
c = b - 1; // c = 'g'
i = b - a; // i = 7

c = 'd';
if ('a' <= c && c <= 'z')
c += 'A'-'a'; // ==> c = 'D';
}

 Analog zu Java können arithmetische Operationen auf Daten


vom Typ char angewendet werden.

38
Explizite Casts

 Ein Wert eines Datentyps kann explizit in einen Wert eines anderen
Datentyps umgewandelt werden, indem man einfach den neuen Typ in
Klammern vor den entsprechenden Ausdruck schreibt.

int main(void){
int i = 3, j = 4, k;
double x = 2.5, y;

y = (double) i + (double) j; // 3.0 + 4.0 = 7.0


y = (double) (i+j) // 3+4 = 7 ==> 7.0 (*)
y = 3.5;
i = (int) x + (int) y; // 2 + 3 = 5 ==> 5
i = (int) (x+y) // 2.5 + 3.5 = 6.0 ==> 6 (*)
}

39
3. Ausdrücke und Anweisungen

 Basis von C sind Ausdrücke


 Ein Ausdruck liefert einen Wert eines Typs.
 Beliebige Ausdrücke können mit Hilfe eines Semikolons zu
Anweisungen gemacht werden – auch wenn das keinen Sinn macht.

Ausdrücke: Anweisungen:

Summe a + 1 a + 1; Macht keinen Sinn.


Inkrement index++ index++; Sinnvoll!
Zuweisung c = 5 c = 5; Sinnvoll!
Test auf Gleichheit a == b a == b; Macht keinen Sinn.

Zusätzliche Beispiele a > b

für Ausdrücke: a*b/c <> c+d*e


(a+b)*3/2
40
Syntax von (einfachen) Zuweisungen

Zuweisung:

Bezeichner = Ausdruck

Eine Zuweisung ist selbst wieder ein Ausdruck! Der Wert des
Zuweisungsausdrucks ist gleich dem Wert der Variable.

float f = i = 42.42;

Ist daher eine erlaubte Kettenzuweisung und wird von rechts


nach links abgearbeitet.
Die Zuweisung ist rechts-assoziativ!

41
Syntax von Variablendeklarationen

Deklaration ohne Anfangswertzuweisung:

Datentyp-Bezeichner Bezeichnerliste ;

Listenelemente sind jeweils


Deklaration mit Anfangswertzuweisung: durch Kommas getrennt.

Datentyp-Bezeichner Zuweisungsliste ;
Deklaration mit oder ohne Anfangswertzuweisung können gemischt auftreten.

int a = 1, b = 2, c = 3;
int zahl;
unsigned long int universalZahl = 42;
char zeichen='d';
float epsilon;
double dd = 0.1;
42
Syntax von Konstantendeklarationen

const Datentyp-Bezeichner Zuweisungsliste ;

const float pi = 3.141592654;


const int universalZahl = 42;

 Unterschied zu Java
 Konstanten müssen bei ihrer Deklaration bereits einen Wert
zugewiesen bekommen.

43
Ausdrücke

 Mit Hilfe von Operationszeichen (Operatoren), Variablen und


Konstanten lassen sich (wohlgeformte) Ausdrücke bilden.
 Eine weitere Komponente von Ausdrücken sind wie in Java die
Funktionsaufrufe.
 Beispiele: ggt(42,81);
 Ein Ausdruck verfügt wie in Java über
 einen Wert
 einen Typ
 Die Auswertung komplexer Ausdrücke in C entspricht dem Ablauf
in Java.
 Die Präzedenz der Operatoren regelt ihre Priorität.
 Wichtig für die korrekte Auswertung ist ihre Assoziativität!

44
Beispiele

a+3*b/c-5*d bzw. (a+((3*b)/c))-(5*d)

 Präzedenz: -
 Punkt- geht vor Strichrechnung.
*
 Links-Assoziativität: +
 Bei gleicher Präzedenz werden die
d
Operatoren von links nach rechts 5
a /
abgearbeitet.
 Veranschaulichung durch
Operatorbäume c
*
 Berechnung durch einen
Postorder-Durchlauf des Baums 3 b

45
Präzedenz (1)

 Bei Programmiersprachen gibt es i.A. viele Regeln für die Präzedenz und
Assoziativität von Operatoren.
 Die folgende Tabelle erläutert die bei C und C++ verwendeten
Präzedenzen - auch für Operatoren, die wir noch gar nicht diskutiert
haben:
Präzedenz Assoziat. Operatoren Funktion

16 links ->, . Auswahloperatoren


16 links [] Array-Index
16 links () Funktionsaufruf
15 rechts sizeof Größe in Bytes
Bindung

15 rechts ++, -- Inkrement, Dekrement


15 rechts ~ bitweises NOT
15 rechts ! logisches NOT
15 rechts +, - unäres Minus- und Pluszeichen
15 rechts *, & Dereferenzierung, Adreßoperator
15 rechts () Typvorgabe (cast)
46
Präzedenz (2)
Präzedenz Assoziat. Operatoren Funktion
14 links ->*, .* Auswahloperatoren für Zeiger
13 links *, /, % multiplikative Operatoren
12 links +, - Addition und Subtraktion
11 links <<, >> Bitverschiebung
10 links <, <=, >=, > relationale Operatoren
9 links ==, != Gleichheit, Ungleichheit
8 links & bitweises AND
7 links ^ bitweises XOR
Bindung

6 links | bitweises OR
5 links && logisches AND
4 links || logisches OR
3 links ?: arithmetischer if-Operator
2 rechts =, *=, /=, +=, -=, <<=, >>=, &=, |=, ^=
Zuweisungsoperatoren
1 links , Komma-Operator
47
Zuweisungsoperatoren

 Die Zuweisungsoperatoren von C/C++ sind rechts-assoziativ.


 Anderen Operatoren sind links-assoziativ - dies ist die normale
Reihenfolge "von links nach rechts".
 Im Allgemeinen gilt dabei: Auf der rechten Seite steht ein Ausdruck
und auf der linken Seite eine Variable.
 Neben dem normalen Zuweisungs-Operator gibt es für
arithmetische Operatoren noch erweiterte Zuweisungs-
Operationen.
v += a ist äquivalent zu v = v + a

v *= a ist äquivalent zu v = v * a

 Besonders häufig werden diese für a = 1 verwendet.


 Inkrementierung eines Werts um 1

48
Die Operatoren ++/--
++, --

 Die Inkrement/Dekrement-Operationen bewirken das Erhöhen


bzw. Erniedrigen einer Variablen um 1.
 Sie können als Ausdrücke und als selbstständige Anweisungen
benutzt werden.
 Seiteneffekte
 Diese Operationen gibt es in zwei Varianten: Postfix und Präfix.
 Diese unterscheiden sich durch den Zeitpunkt, an dem das Ergebnis des
Ausdrucks ermittelt wird.
 Präfix: Wende die Operation vorher an. Das Ergebnis des Ausdrucks
ist der Wert der Variablen nach der Operation.
 Postfix: Wende die Operation nachher an. Das Ergebnis des
Ausdrucks ist der Wert der Variablen vor der Operation.

char c = 40; char c = 40;


while (++c <= '1'){ while (c++ <= '1'){
... ...
} }
49
Beispiele

Präfix Postfix
int i = 42; int i = 42;
int a = ++i; int b = i++;

i hat den Wert ? i hat den Wert ?


a hat den Wert ? b hat den Wert ?

 ACHTUNG
 Die Verwendung von mehr als eines solchen Operators in einem
Ausdruck ist problematisch und sollte vermieden werden.

50
Der Datentyp bool in C99

 In C99 gibt es auch den Datentyp bool


 Repräsentation von Booleschen Werten true und false
 false wird durch die ganze Zahl 0
repräsentiert.
 true wird durch die der Zahl 1
repräsentiert.
 Voraussetzung für die Nutzung ist die Präprozessor-Direktive:
#include<stdbool.h>
 Dekaration von Variablen und Initialisierung
bool b = true;
 Da bool im Wesentlichen durch ganze Zahlen repräsentiert
wird, machen auch folgende Befehle Sinn.
int i = 42;
bool b = i; // b hat den Wert true (== 1)
i = b; // i hat jetzt den Wert 1
51
Boolesche Operatoren

 Relationale Operatoren liefern einen Booleschen Wert.

< , > Test auf kleiner bzw. größer


<= , >= Test auf kleiner/größer oder gleich
!= Test auf ungleich
== Test auf gleich

 Boolesche Ausdrücke setzen sich aus atomaren Formeln wie z. B.


a==b oder c<=5 zusammen.
 Diese atomaren Formeln können dann mit !, && und || zu
komplexeren Ausdrücken verknüpft werden.

52
Auswertung der Ausdrücke

Besonderheit bei && und ||


 Bei && wird der zweite Term nur ausgewertet, wenn der erste true
ergeben hat!
 Bei || wird der zweite Term nur ausgewertet, wenn der erste false
ergeben hat!
 Beispiele
if ((a != 0) && ( 5/a > 1)) { ... }
if ((a == 0) || ( 5/a > 1)) { ... }

53
Shift-Ausdrücke

 Vorraussetzung
 2-er Komplementdarstellung ganzer Zahlen.
 >> und << sind Shift-Operatoren. Sie veranlassen die bitweise
Verschiebung des ersten ganzzahligen Operanden.
 << Verschieben der Bits nach links und Auffüllen mit Bit 0
 >> Verschieben der Bits nach rechts und Auffüllen mit dem
Vorzeichenbit
 Der zweite Operand (positive Zahl) gibt an, um wie viele Stellen der
erste Operand nach links bzw. rechts verschoben wird.

16 = 100002
int zahl1 = 16, zahl2 = 42; 42 = 1010102
int neu1 = zahl1 << 5;
int neu2 = zahl2 >> 3; 512
5

54
Noch mehr Zuweisungsoperatoren

 Bekannt sind bereits die Zuweisungsoperatoren in


Kombination mit arithmetischen Operationen.
 Zuweisungsoperatoren mit Shift
 x <<= stellen x = x << stellen
 x >>= stellen x = x >> stellen
 Logische Zuweisungsoperatoren
 x &= y x=x&y
 x |= y x=x|y
 x ^=y x = x^y

55
Bedingte Ausdrücke – der ?-Operator

Bedingter Ausdruck :

Bedingung ? Ausdruck1 : Ausdruck2

B ? A1 : A2
 Dreistellige Operation mit den Operanden B, A1 und A2.
 B liefert einen Booleschen Wert.
 Beim Wert true ist das Ergebnis der zweite Operand, also A1
 Wenn der Wert false ist das Ergebnis der dritte Operand, also A2.
 Beispiele

int a = 5, b = 7; int f(int n){


... return (n <= 0) ? 1 : n*f(n-1);
int max = (a>b) ? a:b; }
56
Komma-Operator

 Ein Komma-Ausdruck besteht aus einer Folge von


Ausdrücken, die durch Kommata getrennt werden.
 Auswertung von links nach rechts
 Das Ergebnis ist das Ergebnis des am weitesten rechts stehenden
Ausdrucks.

 Beispiel:

int a, max, i, j;
....
max = ( i > j ) ? ( a -= i , i )
: ( a += j , j );

57
4. Kontrollstrukturen

 Verbundanweisungen
 Bedingte Anweisungen
 Schleifen
 while
 for
 do-while
 switch-Anweisungen
 break, continue, goto

58
Warum Kontrollstrukturen

 Ein Programmtext ist eine lineare Folge von Befehlen.


 Der Ablauf eines Problems durchläuft die Befehle i. A. nicht
linear, sondern abhängig vom Ergebnis eines Befehls wird
zu dem nächsten Befehl gesprungen.
 Dies wird auch als Kontrollfluss bezeichnet.
 Beliebige Sprünge sind sehr gefährlich beim Programmieren,
aber in C möglich.
 goto-Anweisung (siehe unten)
 Stattdessen stellen höhere Programmiersprachen andere
Kontrollstrukturen zur Verfügung, um in geeigneter Weise
Sprünge im Programmtext zu erlauben.

59
Die 3 Kontrollstrukturen

 Drei grundlegende Kontrollstrukturen reichen aus, um alle


berechenbare Programme zu implementieren.
Sequentielle Komposition, (Verbundanweisung),
Bedingte Anweisung (if-Anweisung)
Schleife (while-Schleife).
 Kontrollstrukturen sind keine Ausdrücke und liefern (anders
als z. B. die Zuweisung) keinen Wert.
 Alle weiteren Kontrollstrukturen sind nicht notwendig,
erleichtern aber die Programmierung.

60
Verbundanweisung

 Sind A1, A2, ... , An Ausdrücke, dann ist eine daraus


abgeleitete Verbundanweisung

{A1;A2;…;An;}

Diese wird auch Block genannt.


 Mit den geschweiften Klammern werden mehrere
Anweisungen logisch zu einer neuen Anweisung
zusammengefasst.
 Überall da, wo mehrere Anweisungen stehen sollen, aber nur
eine stehen darf, kann man einen Block von Anweisungen
nutzen.

61
Bedingte Anweisung

if ( Bedingung )

Anweisung-1 else Anweisung-2

 Die Bedingung wird ausgewertet. Wenn das Ergebnis true ist,


wird die Anweisung-1 ausgeführt ansonsten die Anweisung-2.
 Optional kann der else-Zweig weggelassen werden. Dann wird
nur, wenn das Ergebnis true ist, die Anweisung-1 ausgeführt

62
Klammerung von if-Anweisungen (1)

 Die Alternativen einer bedingten Anweisung können


beliebige, also auch wieder bedingte Anweisungen sein.
 Auf diese Weise können ganze Kaskaden von Alternativen
entstehen.
 In dem folgenden Beispiel wird eine dreifach geschachtelte
Alternativanweisung gezeigt:

if (jahr % 4 != 0) tage = 365;


else if (jahr % 100 != 0) tage = 366;
else if (jahr % 400 != 0) tage = 365;
else tage = 366;

 In einem solchen Fall gilt grundsätzlich in allen bekannten


Programmiersprachen die Regel:
 Jedes else bezieht sich grundsätzlich auf das links davon
stehende nächstliegende if.

63
Klammerung von if-Anweisungen (2)

 Somit gibt es folgende Bezüge zwischen den if-else-


Anweisungen:
if (jahr % 4 != 0) tage = 365;
else if (jahr % 100 != 0) tage = 366;
else if (jahr % 400 != 0) tage = 365;
else tage = 366;

Konvention
 In solchen Fällen sollte die Formatierung den Bezug
zwischen if und else reflektieren.

64
while-Schleife

Syntax

while ( Bedingung ) Anweisung

Semantik
 Die Bedingung wird ausgewertet. Wenn das Ergebnis true ist,
wird die nachfolgende Anweisung ausgeführt. Andernfalls ist die
while-Anweisung beendet. Im ersten Fall wird die Bedingung
erneut ausgewertet und die Vorgehensweise wiederholt.

65
Beispiele

while (Bedingung) A1; A2; Nur A1 wird ggf. wiederholt


ausgeführt!

while (Bedingung) { A1; A2; } A1 und A2 werden ggf.


wiederholt ausgeführt!

while (x != y) Kombination von while und if.


if (x > y) A1 Die gesamte if-Anweisung ist
x -= y; eine einzige Anweisung A1.
else
y -= x;

66
Bedingungen

 Bedingungen sind Ausdrücke vom Typ bool.


 Da ein int-Ausdruck implizit in bool übertragen wird, darf in
einer Bedingung einen Ausdruck vom Typ int stehen.
 0 wird als false und alle anderen Werte als true interpretiert.
 Beispiel
Zuweisung mit gleichzeitiger
int i = 10, zweipot = 1; arithmetischer Operation.
while (i--) zweipot *= 2;

ist eine Abkürzung von

int i = 10, zweipot = 1;


while ((i--) != 0) zweipot = 2*zweipot;

67
Typischer Fehler

 Häufig treten bei C Programmierfehler mit Bedingungen auf,


da statt "==" fälschlicherweise "=" benutzt wird.
 Programmbeispiel
int f(int n){
// Berechnung der Fakultät für n > 0
return (n == 1) ? 1 : n*f(n-1);
}
f(5)  120

 Irrtümliche Verwendung des Zuweisungsoperators

int f(int n){


// Berechnung der Fakultät für n > 0 ?
return (n = 1) ? 1 : n*f(n-1);
f(5)  1
}

68
do-while-Schleife

Syntax

do Anweisung

while ( Bedingung ) ;
Semantik
 Zunächst wird stets die Anweisung ausgeführt. Danach wird
geprüft, ob die Bedingung true ergibt. Wenn ja, wird die Schleife
wiederholt. Ansonsten wird die Schleife beendet.
Beispiel
do {
sum += powx/ifac;
powx *= x;
ifac *= i++;
} while (powx/ifac > eps);
69
for-Schleife

Syntax

for ( Init-Ausdruck ; Bedingung ;

Step-Ausdruck ) Anweisung

Semantik
 Definition durch Zurückführen auf die while-Schleife
Init-Ausdruck;
while (Bedingung) {
Anweisung
Step-Ausdruck;
}
 Eigentlich braucht man die for-Schleife nicht, aber
Programme werden dadurch kürzer und verständlicher.

70
Beispiele für for-Schleifen

int i, zweipot = 2;
for (i = 1; i < 10; i++ ) zweipot *= 2;
printf("%d\n", zweipot);

 for-Schleifen werden häufig in Zusammenhang mit den noch zu


besprechenden Arrays benutzt:

// Deklaration und Initialisierung der Variable


int i, zahlen[100];
for (i = 0; i < 100; i++) zahlen[i] = i*i;

// Suche im Array
bool gefunden = false;
for (i = 0; !gefunden && (i < 100); i++)
if (zahlen[i] == 49) gefunden = true;

71
Vollständiges Beispiel

 Teste die Syrakus Folgen von 3 bis 30 (nur ungerade Werte )


 Wenn der Vorgänger x ungerade ist, ist 3x+1 das nächste
Folgenelement. Ansonsten (x gerade) ist x/2 das Element.

#include <stdio.h>
int main (void){
int sy, sytest;
for (sy = 3; sy < 30; sy += 2 ){
printf("Neuer Test mit: %d\n",sy);
sytest = 3*sy+1;
while (sytest > sy) {
printf("%d ",sytest);
sytest = (sytest & 1) ? 3*sytest + 1 : sytest/2;
}
printf("%d\n",sytest);
}
72
}
Die switch-Anweisung

 Oft muss man unter Verwendung des Ergebnis eines Ausdrucks


eine Unterscheidung in viele Fälle vornehmen.
 Durch geschachtelte bedingte Anweisungen kann man dies
ausdrücken, das Programm wird dabei aber unübersichtlich.
 Für derartige Fallunterscheidungen gibt es in C bzw. C++ die
switch-Anweisung.
 Entsprechende Anweisungen gibt es auch in anderen
Sprachen (wie z. B. Java und Pascal)

73
Syntax der switch-Anweisung

switch-Anweisung:

switch ( Switch-Ausdruck ) { Fälle }

 Der Typ des switch-Ausdruckes muss ganzzahlig sein.


 Für beliebige mögliche Ergebniswerte dieses Ausdruckes
kann ein Fall formuliert werden.
 Jeder Fall besteht aus einer Konstanten und einer Folge von
Anweisungen.
 Schließlich ist noch ein Default-Fall erlaubt.
Fälle:

case Konstante : Anweisungsfolge

default :
74
Semantik der switch-Anweisung

 Wenn der switch-Ausdruck den Wert eines der Fälle trifft, so wird
die zugehörige Anweisungsfolge und die aller folgenden Fälle(!)
ausgeführt, ansonsten wird, falls vorhanden, der Default-Fall und
alle folgenden Fälle (!) ausgeführt.

Problem der switch-Anweisung


 Nicht nur die Anweisung eines Falles, sondern die Anweisungen
aller auf einen Treffer folgenden Fälle werden ausgeführt!

Lösung des Problems


 Soll immer nur genau ein Fall ausgeführt werden, muss man
jeden Fall mit einer break-Anweisung (oder einer return-
Anweisung, siehe später) enden lassen.
 Mit einer break-Anweisung verlässt man den durch die switch-
Anweisung gebildeten Block.

75
Beispiel

#include <stdio.h>

int main (void){


int n = 42;
switch ( n ){
case 1: printf("Fall-1\n");
case 13: printf("Fall-13\n");
case 42: printf("Fall: universal\n");
case 4711: printf("Fall-4711\n");
default: printf("Default-Fall\n");
}
}
Fall: universal
Fall-4711
Default-Fall

76
Beispiel mit break-Anweisung

 Wenn man nur einen Fall ausführen will, benötigt man am Ende
jeder Fall-Anweisungsfolge eine break- oder return-Anweisung!
#include <stdio.h>

int main (void){


int n = 42;
switch ( n ){
case 1: printf("Fall-1\n"); break;
case 13: printf("Fall-13\n"); break;
case 42: printf("Fall: universal\n"); break;
case 4711: printf("Fall-4711\n"); break;
default: printf("Default-Fall\n");
}
}

Fall: universal
77
goto-Anweisung

 In C gibt es noch die aus den Maschinensprachen bekannte


goto-Anweisung.
 Jedem Befehl kann eine Marke (Label) vorangestellt werden.
Dadurch wird ein eindeutiger lokaler Bezeichner definiert.
 Hinter dem Schlüsselwort goto steht eine Marke.
 Bei Ausführung der goto-Anweisung wird an die entsprechende
Marke gesprungen.
 Die goto-Anweisung ist sehr fehleranfällig und sollte deshalb
immer vermieden werden.
 Als Alternativen bieten sich Schleifen und spezielle Sprungbefehle,
wie z. B. break, continue und return an.

78
Die Sprunganweisung break

 Die Sprunganweisungen break und continue beziehen sich


auf eine Schleife (do, while, for).
 break Beispiel
 Erzwingt einen Abbruch der Schleife. for (i=0;;i++) {
Die Verarbeitung wird bei der nächsten …
hinter der Schleife stehenden Anweisung // Abbruch der Schleife
if (i > 42) break;
fortgesetzt.

}

Verschachtelte Schleifen: Java vs. C/C++


In Java:
Definition einer Sprungmarke (Label) vor dem Schleifenkopf der äußeren Schleife
Durch Angabe der Marke hinter dem break wird bei Ausführung der break-
Anweisung die markierte Schleife verlassen, auch wenn die break-Anweisung
in der inneren Schleife steht.
In C/C++:
break verlässt immer die direkt umgebende Schleife
Sprungmarken können nur durch goto erreicht werden
79
Die Sprunganweisung continue

 Die Anweisung continue erzwingt ein Abbruch des aktuellen


Schleifendurchlaufs. Die Verarbeitung wird beim nächsten
Schleifendurchlauf fortgesetzt.
Beispiel
for (w1 = wuerfeln();;) {
sum += w1;
if (w1 > 3)
continue;
else {
w2 = wuerfeln();
if (w2 < 3) break;
}
}

Verschachtelte Schleifen: Java vs. C/C++ (2)


In Java kann durch Angabe einer Sprungmarke hinter continue die Schleife definiert
werden, deren Iteration durch continue fortgesetzt werden soll.
In C/C++ veranlasst continue lediglich den nächsten Schleifendurchlauf der direkt
umgebenden Schleife. Die Sprungmarke einer äußeren Schleife wird durch goto erreicht.
80
5. Formatierte Ein- und Ausgabe

 In der Datei stdio.h sind wie Funktionen für die Ein- und
Ausgabe definiert. Insbesondere findet man hier auch die
Funktionen für die formatierte Aus- und Eingabe.
 printf
 scanf
 In stdio.h gibt es noch weitere Funktionen, die aber erstmal
für uns nicht von Interesse sind.

81
printf

 Zweck
 Formatierte Ausgabe von Variablen und Konstanten.
 Syntax
Aufruf der Funktion printf:

printf ( Formatstring , Argument )

 Die Funktion liefert als Ergebnis die Anzahl der ausgegebenen


Zeichen zurück.
 Beispiel (Syrakus)
printf("Neuer Test mit: %d\n",sy);

82
Formatstring (1)

 Der Formatstring setzt sich aus Text und


Formatanweisungen zusammen.
 Eine Formatanweisung besteht aus einem Prozentzeichen
und einem weiteren Zeichen für den Datentypen.
 Die Formatanweisung wird durch das nächste noch nicht benutzte
Argument der printf-Funktion ersetzt.
 Beispiele für einfache Formatanweisungen
Datentyp int
 %d, %i Ausgabe als Dezimalzahl
 %o Ausgabe als Oktalzahl ohne Vorzeichen
 %x,%X Ausgabe als Hexadezimalzahl
 %u Ausgabe ganzer Zahlen ohne Vorzeichen
 %c Ausgabe ganzer Zahlen als Unicodezeichen.
Datentyp long int
 %ld, %lo, %lx, %lu, %lc

83
Formatstring für Gleitpunktzahlen

Datentyp float und double


 %f Ausgabe von float und double im Format
[-]mmm.ddd (# Nachpunktstellen = 6).
 %e Ausgabe von float und double im Format
[-]m.dddexx (#Nachpunktstellen= 6)
 %g Ausgabe von float und double im Format %e,
wenn der Exponent kleiner als -4 bzw. größer als
6. Sonst %f.

 %8f Anzahl der Stellen mindestens 8, davon 6


Nachpunktstellen
 %8.4f Anzahl der Stellen insgesamt 12, davon 4
Nachpunktstellen

Datentyp long double


 %lf,%le,%lg siehe oben

84
Beispiel (printf)

#include<stdio.h>

int main() {
char c;
int i;
double d;
c = 'S';
i = 31;
d = 1.23456789;
printf("c als Zahl: %d\n", c);
printf("i hexadezimal: %x\n", i);
printf("d mit Vorz. und 5 Nachkommast.: %6.5e\n", d);
}

85
Dateneingabe mit scanf

 Zweck
 formatierte Eingabe von Werten für Variablen
 Syntax
Aufruf der Mehode scanf:

scanf ( Formatstring , Argumentadresse )

 Rückgabewert:
 Anzahl der tatsächlich eingelesenen Eingaben
 Eingaben (evtl. Fehlerkorrektur)
 Syntax: int scanf (formatstring [,parameter])

86
Der Formatstring von scanf

 Der Formatstring besteht aus Formatanweisungen, Leerzeichen


und anderen Zeichen.
 Formatanweisungen bestehen aus Prozentzeichen % und
Zeichen, das den Datentyp der Eingabe angibt.
 Leerzeichen passen zu einem Leerraum jeder Größe
 Andere Zeichen passen nur zu sich selbst
 Die Zeichen hinter dem Prozentzeichen beschreiben den
erwarteten Datentyp.
 Eingaben werden an die als Argument übergebenden Adressen
geschrieben.
 Im Vorgriff auf die Diskussion zu Pointern soll hier bereits erwähnt
werden, dass zu einer Variable v die zugehörige Adresse durch &v
gegeben ist.

87
Wichtige Formate für scanf

 Die Formatanweisungen entsprechen im Wesentlichen denen


von printf.
%d Dezimalzahl mit Vorzeichen
%i Dezimal-, Oktal oder Hexadezimalzahl
%u Dezimalzahl ohne Vorzeichen
%o Oktalzahl ohne Vorzeichen
%x, %X Hexadezimalzahl
%c Zeichen
%s Zeichenkette (mit ‘\0‘ abgeschlossen)
%f,%e Fließpunktzahlen in entsprechender Schreibweise

88
Beispiel (scanf)

#include<stdio.h>
Übergabe der
Adressen der
int main(void) { Variablen
int alter, d, m, y;
printf("Wie alt bist Du? ");
scanf("%d", &alter);
printf("Du bist %d Jahre alt.\n", alter);
printf("\nGeburtsdatum (xx|xxxx|xxxx) ");
scanf("%d|%d|%d", &d, &m, &y);
printf("Du hast am %d.%d.%d Geburtstag\n", d,m,y);
}

89
6. Datentypen

Inhalt
 Aufzählungen
 Typdefinitionen
 Arrays
 Strukturen ("struct")
 Pointer und Referenzen
 Dynamische Datenstrukturen

90
Motivation

 Neben Kontrollstrukturen bieten Programmiersprachen


Mechanismen zur Erzeugung benutzerdefinierter Datentypen
aus elementaren Typen.
 Elementare Datentypen in C: int, float, ...
 Konstruktoren für Datentypen: array, record, pointer, ...
 Neue Datentypen bieten eine natürlichere Abbildung der Objekte aus
der realen Welt.
 Benutzerdefinierte Typen bieten die gleiche Funktion wie die
bereits bekannten elementaren Typen:
 Deklaration von Variablen
 Wert eines Ausdrucks kann als Typ einen benutzerdefinierten Typ
besitzen.
 Benutzerdefinierte Typen können über einen Namen angesprochen
werden.

91
Aufzählungen

Aufzählung:

enum Enum-Name { Werte-Liste } Var-Liste ;

 Ein Aufzählungstyp kann einen Bezeichner als Namen haben.


 Die Werteliste besteht aus einer Folge von ganzzahligen
Konstanten. Man unterscheidet zwischen
 Implizite Wertebelegung, wenn keine explizite Zuweisung der Werte
 Beginnend bei 0 werden die Werte aufsteigend vergeben.
 Explizite Wertebelegung durch explizite Zuweisung der Werte
 Hybride Wertebelegung:
 Mischform aus impliziter und expliziter Belegung
 Deklaration von Variablen:
 Direkt bei der Typdeklaration oder später unter Verwendung des
Typnamens (und des Typkonstruktors)

92
Beispiele

enum Stadt {MARBURG, GIESSEN, FRANKFURT, KASSEL} a, b, c;

 Definition eines neuen Datentyps mit Namen Stadt


 Werte sind Konstanten (vom Typ int) mit den Namen
MARBURG = 0, GIESSEN = 1, FRANKFURT = 2, KASSEL = 3.
 Deklaration von drei Variablen a, b und c.

enum {UNIVERSAL = 42, K = 64, M = 64*64, N};

 Ein solch namenloser Typ darf definiert werden.


 Es können nur Variablen bei der Typdefinition deklariert werden.
 Man darf im Programm auf die Konstanten zugreifen.
 Aufgrund der hybriden Wertebelegung bekommt N den Wert 4097.

93
Komplettes Beispiel

#include <stdio.h>
enum Stadt {MARBURG, GIESSEN, FRANKFURT, KASSEL};
enum { UNIVERSAL = 42, K = 1024, M = 1024*1024, N };
/* Typdefinition ohne Namen */
/* Hybride Wertebelegung: N = M+1 */
int main (void) {
enum Stadt meineStadt = MARBURG, deineStadt = KASSEL;
/* Deklaration von Variablen und Initialisierung */
int m = meineStadt; // impliziter Cast ohne Warnung
int d = (int) deineStadt; // expliziter Cast
printf("%d %d\n", m, d); 0 3
m = M; // Konstante M ist definiert!
d = N;
printf("%d %d\n", m, d); 1048576 1048577
}

94
Anmerkungen

 Aufzählungstypen werden i. A. mit impliziter Wertebelegung


definiert.
 Bei Definition eines Datentyps sollte der Typ in irgendeiner
Weise auch verwendet werden (z. B. Deklaration von
Variablen).
 Typen sollten zumindest dann einen Namen erhalten, wenn
diese im Programm mehrmals verwendet werden.
 Vermeidung von äquivalenten Typdefinitionen

95
Typ-Definitionen

 Beliebige Typen können einen neuen Namen zugewiesen


bekommen.
typedef Typdeklaration Typname

 Beispiele:
Typdeklaration
typedef unsigned int Word;
// Umbennenung des Datentyps unsigned int
typedef double Real; // Umbenennung von double
typedef enum { MARBURG, GIESSEN } Stadt;

Variablendeklaration
Word vw;
Real vr;
Stadt vb;
96
Unterschiede zu Java

 In Java können neue Datentypen nur in Form einer Klasse


definiert werden.
 Klassen gibt es in C nicht!

97
6.1 Arrays

 Zu einem Datentyp T kann eine Array-Variable definiert


werden:
T name [ MAX ]

 name ist eine Array-Variable mit Basistyp T.


 Der Indexbereich der Variable geht von 0 .. (MAX-1) .
 Variable name besteht also aus MAX Elementen vom Typ T.
 MAX muss eine Konstante oder ein konstanter Ausdruck
sein
(d. h. zur Übersetzungszeit berechenbar sein)
 Zugriff auf Elemente im Array :
T x = name[i];
name[i] = x;
 Arrayzugriff erfolgt oft in einer for-Schleife
for (i=0; i < MAX; i++) { x = name[i]; ... }

98
Initialisierung von Arrays (1)
 Initialisierung von Array-Variablen bei der Deklaration:

T name[MAX] = {Wert0, Wert1,....,Wert_MAX_Minus_1};

Die Werte müssen zu T passen, d. h.


 entweder der Typ der Werte muss mit T übereinstimmen
 Typen der Werte können implizit in T konvertiert werden.

 Diese Form der Initialisierung kann nur bei der Deklaration


verwendet werden.

99
Initialisierung von Arrays (2)

 Wenn Arrays bei der Deklaration initialisiert werden, sollte man


das Zählen der Elemente, wenn möglich, dem Compiler
überlassen !
Beispiele: double x[] = { 12.2, 45.4, 67.2, 12.2, 34.6, 87.4,
83.6, 12.3, 14.8, 55.5 };
char str[] = “Dies ist ein Text”;
int a[] = { 29, 28, 27, 26, 25, 24, 23, 22, 21, 20 };

sizeof(x);
Zugriff auf die Anzahl der Bytes:
sizeof(str);
sizeof(a);

int nx = sizeof(x) /sizeof(x[0]);


Zugriff auf die Anzahl
int ns = sizeof(str)/sizeof(str[0]);
der Elemente:
int na = sizeof(a) /sizeof(a[0]);
100
Array - Beispiele

#include <stdio.h>
int main (void){
int array[10]; /* Integer-Array array[0] .. array[9] */
double zahlen[300]; /* Double-Array zahlen[0] .. zahlen[299] */
char text[20]; /* Char-Array Text[0] .. Text[19] */
int i, x;
array[7] = 3;
x = array[5];
array[10] = 7;
/* semantischer FEHLER! array[10] existiert nicht. */
for (i = 0; i < 300; i++)
zahlen[i] = i; /* Zuweisung in einer Schleife */
text[4] = 'c';
}

101
Typdefinitionen mit Arrays

Basistyp Typname [ Anzahl der Elemente ]

typedef int IntArray [10];


/* definiert einen Datentyp mit 10 Elementen vom Typ int. */

IntArray a = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
/* Deklaration einer Array-Variable a mit folgendem Inhalt */

Inhalt: 1 2 3 4 5 6 7 8 9 10
Index: 0 1 2 3 4 5 6 7 8 9

102
Beispiele

typedef int IntArray[10]; /* Deklaration des Datentyps IntArray */


typedef char Str[42]; /* Deklaration des Datentyps Str */
typedef double Matrix[47][11]; /* Deklaration des Datentyps Matrix */

Variablendeklaration

IntArray vi;

Str vs;

Matrix vm;

Achtung:
typedef int [10] IntArray; /* ist ein Syntaxfehler */
aber
typedef int IntArray [10]; /* ist korrekt */

103
Besonderheiten von Arrays
 Array-Zuweisungen sind verboten
IntArray a, b;
a = b; /* geht nicht !! */

 Ebenso geht nicht:


IntArray a = { 1, ...., 10 }; /* ok */
IntArray b = a; /* geht leider auch nicht */

 Mehrdimensionale Arrays können ebenfalls definiert werden:


int mehrDim [10] [10] [10]; /* ok */
/* int mehrDim [10, 10, 10] ist verboten */

 Analog der Zugriff:


mehrDim [i] [j] [k];
/* aber nicht mehrDim [i, j, k] */

 i, j, k ist ein "Komma-Ausdruck" mit Ergebnis k.


Daher ist mehrDim [i, j, k] also
äquivalent zu mehrDim [k]
104
Arrays mit chars und Strings

 String-Literale haben ein Zeichen mehr: eine abschließende Null !

 Das folgende Array a besteht daher aus 11 Elementen!

char a[]= "1234567890";

 Die folgenden Arrays c und b haben beide je 10 Elemente.

char c[10] = {'1', '2', '3', '4', '5', '6', '7', '8', '9','0'};

char b[] = {'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'};

 Alle drei Arrays haben denselben Basistyp: char.

105
Vollständiges Beispiel

#include <stdio.h>

int main(void){
double x[] = { 12.2, 45.4, 67.2, 12.2, 34.6, 87.4,
83.6, 12.3, 14.8, 55.5 };
int i, n = sizeof(x) / sizeof(x[0]);
double sumx = 0.0;
printf("Array ist:\n");
Array vorzeigen und Summe der
for (i = 0; i < n; i++) {
Werte berechnen
sumx += x[i];
printf("x[%d] = %f\n",i, x[i]);
}
printf("\nAnzahl der Werte = %d\n", n); Mittelwert berechnen
printf("Mittelwert = %f\n",sumx / n);
}

106
Initialisierung mehrdimensionaler Arrays

 Mehrdimensionale Arrays werden initialisiert, indem jede Dimension für


sich geklammert wird, und die einzelnen Einträge der Dimensionen durch
Kommata getrennt werden. Dies ist etwas knifflig:
int a[2][3][4] = {
{ { 0, 1, 2, 3 }, { 10, 11, 12, 13}, { 20, 21, 22, 23} },
{ {100,101,102,103}, {110,111,112,113}, {120,121,122,123} }
};

 Auch hier kann die Dimensionsangabe wegfallen, allerdings nur bei der
letzten Dimension.
 Ferner ist es möglich einzelne Klammerpaare wegzulassen, der Compiler
weist die Komponenten in ihrer natürlichen Reihenfolge zu. So können
auch alle Klammern weggelassen werden. Empfehlenswert ist es, eine
komplette Klammerung durchzuführen um den Überblick nicht zu verlieren.

int b[2][3][4] = { 0,01,02,03,10,11,12,13,20,21,22,23,


100,101,102,103,110,111,112,113,120,121,122,123 };

int c[2][3][4] = {{{0,01,02,03},10,11,12,13,{20,21,22,23}},


100,101,102,103,{110,111,112,113},120,121,122,123};
107
Unterschiede zu Java

 Arrays sind in Java Objekte


 Objekte gibt es in C nicht.
 Die Größe eines Arrays kann in Java erst zur Laufzeit
festgelegt werden.
 In C muss die Größe bereits zur Übersetzungszeit feststehen.

108
6.2 Strukturen
Struktur:

struct Struct-Name { Komp-Liste } Var-Liste ;

struct Adresse {
Str15 vorname; Vorausgesetzt werden dabei
Str15 nachname; folgende Typdefinitionen:
Str20 strasse;
typedef char Str10[10];
int plz;
typedef char Str15[15];
Str20 stadt;
typedef char Str20[20];
Str10 vorwahl, telNr;
} p1, p2;

 Deklaration eines neuen strukturierten Datentyps mit Namen Adresse


 Deklaration von zwei Variablen p1 und p2 des Typs Adresse
 Werte des Datentyps sind Tupel mit Daten vom Typ
{ Str15, Str15, Str20, int, Str20, Str10, Str10}
109
Zugriff auf Komponenten

 Eine Struktur ist eine Zusammenfassung mehrerer Komponenten zu


einem neuen Datentyp.
 In anderen Sprachen wie Pascal oder COBOL sind Strukturen auch
unter dem Namen Record bekannt.
 Der Zugriff auf die Komponenten erfolgt mit einem Punkt als Selektor.
 int p = p1.plz;
 p1.plz = p;
 Weitere Beispiele für Zugriffe auf diese Strukturen:
strcpy(p1.vorname, "Fritz");
Eine direkte Zuweisung
strcpy(p1.nachname, "Sauer"); p1.vorname = "Fritz";
strcpy(p1.strasse, "Am Krappen"); ist in C nicht erlaubt! strcpy
p1.plz = 35037; erfordert ein
strcpy(p1.stadt, "Marburg"); #include <string.h>
strcpy(p1.vorwahl, "0170"); am Programmanfang.
strcpy(p1.telNr, "3801928");

110
Definitionsvarianten
struct {
Str15 vorname; Direkte Definition einer
Möglichkeit 1:
... Variablen p1 der "Struktur".
Str10 vorwahl, telNr; “Un-Tagged”
} p1;
struct Adresse2 { Definition einer Variablen p1
Str15 vorname; der "Struktur" mit Typname.
Möglichkeit 2:
... “Tagged”
Str10 vorwahl, telNr;
} p1;
typedef struct AdressTag3 { Definition eines Datentyps
Str15 vorname; Adresse und Definition einer
Variablen p1 dieses Typs.
Möglichkeit 3: ...
Str10 vorwahl, telNr; Varianten:
} Adresse3; “Tagged” oder “Un-Tagged”
Adresse3 p1;

111 Diese unterschiedlichen Möglichkeiten sind historisch bedingt !


Variablendeklaration

 Entsprechend den verschiedenen Möglichkeiten zur


Deklaration eines Record-Typen gibt es eine Vielfalt bei der
Deklaration von Variablen:
 Möglichkeit 1
 Für einen namenlosen Typ können keine Variablen deklariert
werden.
 Möglichkeit 2
 struct Adresse2 a2;
 Möglichkeit 3
 Adresse3 a3;
 struct AdresseTag3 a4;

112
Initialisierung
 Ähnlich wie Arrays können auch struct-Variablen bei der Deklaration
initialisiert werden.
 Wenn ein Datentyp Adresse definiert ist, können Variablen p1 und p2
bei der Deklaration folgendermaßen initialisiert werden:

typedef struct { ... } Adresse;


int main (void){
Adresse p1 = { "Fritz", "Sauer", "Am Krappen",
35037, "Marburg", "0170", "3801928"};
Adresse p2 = { "Else", "Sauer", "Am Krappen",
35037, "Marburg", "06421", " 43286 "};
}

113
Unterschiede zu Java

 In Java gibt es keine Records.


 Eine Nachbildung ist nur mittels einer Klasse möglich.

114
6.3 Spezialfall: Bitfelder

 Die Komponenten von Strukturen können als besonderen Typ so


genannte Bitfelder besitzen.
 Motivation hierfür ist:
 Ersparnis von Speicherplatz
 Direkte Abbildung einer Speicherbelegung

115
Regeln

 Bitfelder sind vom Grundtyp [[un]signed] int , wobei zusätzlich


die Anzahl der Bits zur Repräsentation angegeben wird.
 Durch die Anzahl der Bits ist auch der darstellbare Wertebereich
eingeschränkt.
 Aufeinanderfolgende Bitfelder einer Variable werden im Speicher -
wenn möglich - direkt hintereinander abgespeichert.
 Namenlose Bitfelder belegen Speicherplatz, können aber nicht
referenziert werden.
 Namenlose Bitfelder der Länge 0 bewirken, dass das nächste Bitfeld
zu Beginn des nächsten Wortes abgespeichert wird. Man spricht
dann von Alignment.
 Im Vorgriff auf das was noch kommt: Pointer auf Bitfelder sind
nicht gestattet. Ebenso sind statische Bitfelder verboten.

116
Structs mit Bitfeldern : Beispiel

struct Bitfeld {
unsigned int ax:16; /* 16-Bit Register */
unsigned int bl:8; /* 8-Bit Register */
unsigned int bh:8; /* 8-Bit Register . */
unsigned int b1:1; /* Boolean; */
unsigned int b2:2; /* 2-Bit Zahl. */
unsigned int :0; /* nächstes Bitfeld zu Beginn eines Wortes */
unsigned int ex:16;
unsigned int :8; /* Speicherlücke; */
unsigned int dh:8;
};
struct Bitfeld bf;

117
Spezialfall union

 Mit "unions" kann man verschiedene Datentypen "übereinander legen".


Dies kann dazu verwendet werden, um Variablen zu definieren, die zur
Laufzeit unterschiedlich interpretiert werden sollen.
#include <stdio.h>
typedef char Str15[15];
typedef struct {
char typ;
union {
Str15 stringWert; int intWert;
} name; // verpflichtend im C99-Standard
} Eintrag ;
int main (void) {
Eintrag alt = {'S',"Zeichenkette"};
/* Das ist erlaubt, gibt aber eine Warnung! */
Eintrag neu = {'I', 4211 }; /* noch eine Warnung! */
printf("%d\n",sizeof(Eintrag));
}
118
6.4 Pointer

 Bei den bisher vorgestellten Variablen wird bereits zur


Übersetzungszeit der erforderliche Speicherplatzbedarf
berechnet.
 Häufig möchte man aber erst zur Laufzeit eines Programms den
Speicherplatzbedarf einer Variablen festlegen
 Kostenersparnis
 Beispiel
 Ein Variable vom Typ Adresse besitzt ein Feld nachname vom Typ
Str15.
 Nachteil:
 Frau Leutheusser-Schnarrenberger kann nicht repräsentiert
werden.
 Abhilfe könnte z. B. ein Datentyp Str28 sein.
 Zur Repräsentation von Herrn Alt wird zu viel Speicherplatz
benötigt: Datentyp Str4 würde genügen.

119
Pointervariablen

 Abhilfe können so genannte Pointervariablen schaffen, die als Wert


eine Speicheradresse besitzen.
 Unter dieser Speicheradresse kann ein Wert eines Typs
hinterlegt werden.
0 1 2 3 4 5 6 7 8 9 10 11 12 13

int *pi;
 Deklaration einer Pointervariablen
 T *p;
 Die Speicheradresse kann Werte vom Typ T enthalten.

120
Wertzuweisung von Pointervariablen

 Der Speicherbereich, auf den eine Pointervariable verweist,


muss zunächst vom Laufzeitsystem angefordert werden.
 Man spricht von einer dynamischen Variablen.
 Hierzu muss die Funktion malloc aufgerufen werden.
pi = (int *) malloc(sizeof(int));
 Als Argument wird die Anzahl der Bytes angegeben
0 1 ´ 2 3 4 5 6 7 8 9 10 11 12 13
8 42

pi
 Auf den Wert der dynamischen Variable kann über den
Sternoperator zugegriffen werden.
 *pi = 42;
121
Dynamische Variablen
 Beim Programmlauf wird Speicher für die Variablen benötigt. Hierbei
wird unterschieden, ob
 die Größe der Variablen bereits zur Übersetzungszeit bekannt ist. Man
spricht dann auch von statischen Variablen.
 die Größe der Variablen wird erst zur Laufzeit festgelegt. Man spricht
dann auch von dynamischen Variablen.
 Beispiel für dynamische Variablen;
 Verwaltung verschieden langer Zeichenketten, deren Größe erst zur
Laufzeit des Programms bekannt ist.
 Eigenschaften von dynamischen Variablen
 Verwaltung im Heap-Speicher, einem speziellen Speicherbereich
 Bereitstellung von Speicherplatz und Rückgabe der Adresse
 Zurücknahme von nicht mehr benötigtem Speicherplatz.
 Dynamische Variablen sind nur über Pointervariablen indirekt
zugreifbar.

122
Anforderung großer Speicherbereiche

 Man kann mit dem Aufruf von malloc einen größeren


Speicherbereich reservieren.

0 1 2 3 4 5 6 7 8 9 10 11 12 13
8 32

pi = (int *) malloc(sizeof(int)*4);

 Zugriff auf die erste Speicheradresse


 *pi = 32; k = *pi;
 Zugriff auf die j-te Speicheradresse (in Einheiten des Typs)
 *(pi+j) = 42; k = *(pi + j);

123
Rückgabe des Speichers

 Der Speicherplatz einer dynamischen Variablen wird mit


einem Aufruf der Funktion free zurückgegeben.
 Dabei wird die Pointervariable als Argument mitgegeben.
 Danach ist die dynamische Variable nicht mehr zugreifbar.
 Beispiel
#include <stdlib.h>
int main (void) {
char *p = NULL; /* Deklartion einer Pointervariablen */
p = (char *) malloc(28); /* Anforderung von 28 Bytes mit
Typkonversion und Zuweisung der Adresse an die Pointervariable */
free(p); /* Rückgabe des Speicherplatzes*/
}

124
Speicheranforderung in C

#include <stdio.h>
#include <stdlib.h>

int main(void) {
typedef int *IntP; // Typdefinition

IntP a = (IntP) malloc(sizeof(int));


/* Speicherplatzreservierung und Zuweisung der Adresse */
*a = 5; // Initialisierung der Speicherzelle
IntP b = (IntP) malloc(sizeof(int)); *b = 42;
IntP c = (IntP) malloc(sizeof(int)); *c = *a + *b;
IntP d = (IntP) malloc(sizeof(int)); *d = 4711;
printf("4 Zahlen: %d %d %d %d\n",*a, *b, *c, *d);
free(a); free(b); free(c); free(d);
/* Zurückgabe des Speichers */
}

125
Datenstruktur für eine Liste
#include <stdio.h>
#include <stdlib.h>
Für die Allokation von
typedef char Str10[10];
Speicher benötigen man
typedef char Str15[15];
typedef char Str20[20]; stdlib.h.

typedef struct {
Str15 vorname;
Adressen wie in diesem
Str15 nachname;
Kapitel bereits definiert.
Str20 strasse;
int plz;
Str20 stadt;
Str10 vorwahl, telNr;
} Adresse;

Listen-Element
typedef struct Node {
Adresse adr;
struct Node *nachfolger; Listen-Pointer
} ListenElement, *LEP;
126
Auf- und Abbau einer Liste (mit 2 Elementen)

int main(void) {
Adresse a1 = { "Fritz", "Sauer", "Am Krappen",
35037, "Marburg", "06421", "6801738"};
Adresse a2 = { "Hugo", "Beispiel", "Lerchenweg 43",
42442, "Zufall", "06421", "6801738"};
LEP anfang = (LEP) malloc(sizeof(ListenElement));
LEP next, cur = anfang;
Listenelement #1 definieren
anfang->adr = a1;
/* Entspricht dem Zugriff (*anfang).adr = a1; */
anfang->nachfolger = (LEP)malloc(sizeof(ListenElement));
cur = anfang->nachfolger;
cur->adr = a2; Listenelement #2 definieren.
cur->nachfolger = NULL; /* spezieller Wert */
/* Jetzt wird der Speicherplatz zurückgegeben */
for(cur = anfang; cur != NULL; cur = next){
next = cur->nachfolger;
free(cur); Die Liste wird wieder abgebaut.
}
}
127
Pointer auf Struct

In dem vorigen Beispiel haben wir Pointer auf Structs verwendet.

Der Zugriff auf die Felder kann in diesem Fall


über den "Pfeil-Operator„ erfolgen: ->

anfang->adr = a1;

anfang->nachfolger->nachfolger = NULL;

128
Unterschiede zu Java

 Keine Pointer in Java!


 Dynamische Reservierung von Speicher
 NUR bei der Objekterzeugung
 Rückgabe des Speichers
 Nicht notwendig
 Erfolgt implizit durch den Garbage Collector der Virtual Machine
 Programmierer hat keinen expliziten schreibenden Zugriff auf Adressen
 Viele Programmierfehler werden dadurch vermieden.

129
Explizite Adressen

 Im Gegensatz zu Java hat man in C die Möglichkeit direkt die


Adressen von normalen Variablen zu lesen und auch einer
Pointer-Variablen zu zuweisen.
 Beispiele "Referenz-Operator": ∗
und der
"Adress-Operator": &

int a = 42; /* ist eine Integer Variable. */


int ∗pa; /* ist ein Pointer auf ein Integer */
pa = &a; /* Adresse von a wird pa zugewiesen. */
int ∗pa = &a; /* ist eine Kurzform der beiden oberen Befehle */
int b = ∗pa; /* b wird mit dem Wert, auf den
pa zeigt, initialisiert: also 42. */
int &ra = a; /* ra ist eine Referenzvariable.
Es gilt immer ra == a */
C++-Syntax
130
Explizite Adressen (2)

int a = 42, b = 4711;


int c = 1, d = 99;
Ignorieren von Leerzeichen
int∗ pa = &a; /* definiert einen Pointer auf einen Integer Wert */
int ∗ pb = &b; /* gleichwertig !! */
int ∗pc = &c; /* ebenfalls gleichwertig !! */
int∗pd = &d; /* ebenfalls gleichwertig !! */
*-Operator bezieht sich auf eine Variable
int∗ pa1, ia1; /* pa1 ist ein Pointer und ia1 ein Integer Wert ! */
int ∗pa2, ia2; /* Deshalb ist diese Schreibweise suggestiver */
int ∗pa3, ∗pa4; /* Deklaration von zwei Pointer */
&-Operator kann auch bei der Deklaration verwendet werden:
int &ra = a;
Typischerweise sollte bei einfachen Variablen &
nur auf der rechten Seite einer Zuweisung
verwendet werden. C++-Syntax
131
Pointer, Adressen: Beispiel

printf("%d %d %d %d\n",a, b, c, d);


printf("%p %p %p %p\n",pa, pb, pc, pd);
printf("%d %d %d %d\n",*pa, *pb, *pc, *pd);
printf("%d\n",ra);
printf("%p\n",&ra);

42 4711 1 99
0x0064FDF0 0x0064FDE8 0x0064FDE0 0x0064FDD8
42 4711 1 99
42
0x0064FDF0

132
Beispiel

#include <stdio.h>

int main (void){


int a = 42, b = 4711, c, d;
int *pa1, *pb1, *pd1;

pa1 = &a; pb1 = &b; pd1 = &d;

*pd1 = c = *pa1 + *pb1;


/* Entspricht d = c = *pa1 + *pb1; */
printf("%d = %d\n",a, b);
printf("a+b = %d\n",c);
}

133
Der Referenzoperator

∗ ist ein Operator, daher zählen Leerzeichen, links und rechts nicht:

int ∗ pa und int ∗ pa und int ∗ pa

sind daher alle gleichbedeutend und:

int *pa, x /* * bezieht sich nicht mehr auf x ! */

134
Pointers und Arrays

 In C gibt es eine enge Beziehung zwischen Pointervariablen


und Arrayvariablen

int a[10], *pa;


pa = a;
/* Der Wert von *pa ist das erste Element im Array; */
pa = &a[0];
/* Der Wert von pa hat sich nicht verändert! */
x = *(pa+1); /* Zugriff auf das zweite Element im Array */
x = *(pa+i);
x = pa[i]; /* Das geht sogar auch!! */
// Der Wert von x hat sich nicht geändert. //
/* Zugriff auf das i-te Element im Array. Die Zuweisung
entspricht also der folgenden Zuweisung: */
x = a[i];
/* Weiterhin gilt, dass pa + i == &a[i] ist. */

135
Arrays sind Pointer, aber …

 Eine Arrayvariable kann stets auch als Pointer interpretiert werden!


 Der Wert der Variable ergibt sich aus der Adresse des ersten
Elements. Es gilt deshalb stets: a == &a[0].
 Da Arrays auf dem Stack durch die Laufzeitumgebung angelegt
werden, kann sich der Wert von a nicht ändern!
 Folgende Ausdrücke sind gleichwertig:
a[k] und *(a+k)
 Damit kann man ein Array int a[10] auch wie folgt ausgeben:
for (k=0; k<10; k++) printf("%d ", *(a+k));
 Die folgende Variante funktioniert nicht für Arrayvariablen:
for (k=0; k<10; k++) printf("%d ", *a++);
 Wenn man stattdessen eine Pointervariable nutzt, ist die
Inkrementierung jedoch zulässig:
pa = a;
for (k=0; k<10; k++) printf("%d ", *pa++);
136
Pointer auf Arrays

1. int * pa[10];
ist ein Array von 10 Pointern auf je einen Integer-Wert
2. int (*pb)[10];
ist ein Pointer auf ein Array von 10 Integer-Werten

[ ] hat Priorität 16L . daher gilt:


* hat Priorität 15R . int * pa[10];
Entspricht
int * (pa[10]);

137
Beispiel

#include <stdio.h>
int main (void){
int *pa[10], a[] = {29,28,27,26,25,24,23,22,21,20};
int (*pb)[10], b[] = {40,41,42,43,44,45,46,47,48,49};
int i,k;
for (i = 0; i < 10; i++)
pa[i] = &a[i];
pb = &b;
printf("a :\n");
for ( i = 0; i < 10; i++)
printf("%d ", *(pa[i]) );
printf("\n b :\n");
for (k = 0; k < 10; k++)
printf("%d ", (*pb)[k]);
printf("\n");
}
138
Beispiel
#include <stdio.h>
int main (void) {
int k, a[] = {40,41,42,43,44,45,46,47,48,49};
printf("\n Überraschung :\n");
for (k = 0; k < 10; k++)
printf("%d ", *(a+k) );
printf("\n");
}

Alternativ:
#include <stdio.h>
int main (void){
int k, a[] = {40,41,42,43,44,45,46,47,48,49},*p = a;
printf("\n Überraschung :\n");
for (k = 0; k < 10; k++)
printf("%d ", *p++ );
printf("\n");
}
139
Strings und Pointer (1)

 In den meisten Fällen werden Zeichenketten mit Pointern bearbeitet.


 Der Datentyp einer Zeichenkettenkonstante ist char*
also Pointer auf char.

 Wenn p ein Pointer auf eine Zeichenkette ist, dann liefert *p++

der Reihe nach alle Zeichen des Strings und inkrementiert dabei p.
Beim letzten Mal ist der Zeichenwert 0, da jede C-Zeichenk ette m it
einer 0 enden m uss .
 Daher terminiert die Schleife: while (*p++)

140
Strings und Pointer (2)

#include <stdio.h>
int main (void)
{
char Test[] = "Dies ist ein Beispiel.";
/* Str.Länge = 22 */
char *p = Test;
int sl = 0;

while ( *p++) sl++;

printf(" Str.Länge : %d\n",sl);


}

141
Strings und Pointer (3)

Die folgende Variante


zeigt, dass Vorsicht geboten ist!

#include <stdio.h>
int main (void)
{
char * Test = "Dies ist ein Beispiel.";
/* Str.Länge = 22 */
int sl = 0;
while ( *Test++) sl++;
printf(" Str.Länge : %d\n",sl);
sl = 0;
while ( *Test++) sl++;
printf(" Str.Länge : %d\n",sl);
}
142
Nochmal: typedef – und seine Bedeutung

 Genaue Bedeutung
 typedef funktioniert entsprechend einer Variablendeklaration.
 Bei Voranstellung des Schlüsselwortes typedef wird statt eines
Variablennamen ein Typname definiert.
 Von diesem Typnamen können dann Variablen deklariert werden.
#include <stdio.h>

typedef int *(*NewType)[];


int main(void) {
int i1 = 1, i2 = 2, i3 = 3;
int *(array[])={&i1,&i2,&i3};
NewType t = (NewType) &array;
int *pI = (((*t)[1])); /* 2. Feldelement */
printf("%d\n",*pI);
}
143
Nochmal: Speicherbereiche

 Beim Ablauf eines C-Programms werden verschiedene


Speicherbereiche verwaltet.
 Stack-Speicher (Stapel)
 Verwaltung der Variablen, die bei einem Funktionsaufruf bzw.
durch Ausführung eines Blocks angelegt werden.
 Nach Beenden der Funktion (Block) wird der Speicherbereich
automatisch wieder zurückgegeben.
 Heap-Speicher
 Verwaltung der dynamisch zur Laufzeit durch malloc erzeugten
Variablen.
 Rückgabe des Speichers muss explizit durch Aufruf der Funktion
free erfolgen.
 Statischer Speicher
 Darunter versteht man den vom Programm mindestens
erforderlichen Speicher, z. B. für das ausführbare Programm
oder für statische Variablen.

144
Kapitel 7

Funktionen
Inhalt

Unterprogramme
Parameterübergabe
Default-Parameter
Inline
Rekursion
Standardfunktionen

146
7.1 Motivation

 Problemlösung als hierarchischer Prozess


 Zerlegung der Probleme in Teilprobleme
 Unabhängige Lösung der Teilprobleme und Kombination der
Teillösungen zu einer Gesamtlösung
 Ein Unterprogramm ist eine logische Programmeinheit zur
Lösung eines Teilproblems.
 Ein Unterprogramm besitzt einen Namen, mit dem die Problemlösung
abgerufen werden kann.
 Parameter bieten die Möglichkeit, diese auch mit unterschiedlichen
Daten auszuführen.
 Zwei Arten von Unterprogrammen
 Unterprogramme, die einen Wert zurückgeben, nennt man Funktionen.
 Unterprogramme, die keinen Wert zurückgeben, nennt man
Prozeduren.
 In C werden Prozeduren und Funktionen syntaktisch durch den
Ergebnistyp void unterschieden.
 Prozeduren werden hier als Spezialfall von
Funktionen angesehen.

147
7.2 Funktionen/Prozeduren

 Die Definition von Unterprogrammen haben wir bereits in


Beispielen kennen gelernt:

Ergebnistyp Name (Parameterliste) {Anweisungen}

 Wenn der Ergebnistyp void ist, haben wir eine Prozedur


vor uns und ansonsten eine Funktion.
 Als Rückgabetyp einer Funktion sind Arrays nicht erlaubt!
 Dieses Problem kann umgangen werden, wenn die
Funktion einen entsprechenden Pointer zurückliefert.

148
Parameter

 Durch die Möglichkeit, Variablen bzw. Ausdrücke an eine Funktion


zu übergeben - die sogenannten Parameter - kann man
Unterprogramme schreiben, die sehr vielseitig sind.
 Je nach Verwendung eines Parameters spricht man von Eingabe-,
Ausgabe- bzw. Ein/Ausgabe-Parameter bzw. In-, Out- und In/Out-
Parametern.
 Eine andere Charakterisierung ist die der Übergabe per
 Wert (Value)
 Adresse (Referenz)
 Name (nur historisch relevant)
Call-by-value und Call-by-reference werden in C/C++ angeboten.
 Parameter können in C als const charakterisiert werden. Dies
entspricht der Übergabe als "In-Parameter“, wobei der Wert der
zugehörigen Variable in der Funktion nicht verändert werden darf.

149
Anmerkungen und Beispiele (1)

 Beispiel einer einfachen Funktionsimplementierung mit


Parameter:
double sqr (double x) { return x*x; }

 Beispiel für den Funktionsaufruf:


double neun = sqr(3);

150
Anmerkungen und Beispiele (2)

 Beim Aufruf von Funktionen, die keine Parameter besitzen, muss


bei ihrer Deklaration und beim Aufruf trotzdem das leere
Klammerpaar angegeben werden!
Beispiel einer Funktion ohne Parameter:
int intEingabe () {
int n;
scanf("%i",&n);
return n; }

Beispiel für den Aufruf dieser Funktion:


int nn = intEingabe();

 int nn = intEingabe; ist syntaktisch korrekt - führt aber nicht zum


Aufruf der Funktion! Dies ist ein häufig gemachter Fehler!
Hier wird nur die Speicheradresse der Funktion geliefert!

151
Parametertypen
 Die normale Methode der Parameterübergabe ist die Übergabe von
Werten (call by value):
 Beim Funktionsaufruf wird der Wert des Ausdrucks berechnet und in
den lokalen Speicherbereich der Funktion kopiert.
 Besteht der Ausdruck aus nur einer Variablen, so ist der Wert der
Variablen nach dem Aufruf gleich dem Wert vor dem Aufruf.
 Achtung Arrays: Der Wert eines Arrays ist bereits seine Referenz!
 Referenzübergabe (call by reference)
 In C kann dies nur durch Pointervariablen simuliert werden.
 Der Wert der Pointervariable bleibt unverändert.
 Die Variable, auf die verwiesen wird, kann geändert werden.
 In C++ kann dies durch Anhängen von & an den Typ des Parameters
explizit gefordert werden.
 Beim Aufruf dürfen nur Variablen übergeben werden, während
allgemeine Ausdrücke vermieden werden sollen ( Warnung !!).

152
Beispiel
 Wertübergabe
void swap1( int x, int y){
int temp = x;
x = y; y = temp;
}

 Referenzübergabe
void swap2( int *x, int *y){
int temp = *x;
*x = *y; *y = temp;
}
 Referenzübergabe (nur in C++)
void swap3( int &x, int &y){
int temp = x;
x = y; y = temp;
}
153
Beispiel

 Funktionsaufrufe
int a=1,b=2;
swap1 ( a,b);
/* a == 1 und b == 2
´ Es ist nichts passiert! */

swap2(&a,&b);
/* a == 2 und b == 1 */

// Dies ist nur in C++ möglich:


swap3 (a, b);
/* Jetzt wieder:
a == 1 und b == 2 */

154
Parameterlisten

 Eine Parameterliste ist eine durch Komma getrennte


Liste einzelner Parameterpositionen:

ParameterPos , ParameterPos ...


Jede Parameterposition kann mit dem optionalen Hinweis const
1.
beginnen. Dieses veranlasst den Compiler den Parameter lokal
als konstant zu behandeln  In-Parameter.
2. Es folgt der Typ-Name und der Bezeichner des Parameters.

Besonderheiten von C++


• Die Referenzübergabe kann durch Verwendung des &
signalisiert werden.
• In C++ können auch noch Defaultwerte an die Parameter
vergeben werden.
• Bei einem Aufruf müssen dann nicht alle Parameter explizit gesetzt
155 werden.
Prototyp, Aufruf und Implementierung

 Vor dem Aufruf einer Methode muss zumindest der Prototyp


definiert sein.
 Ein Prototyp entspricht der Signatur der Methode (ohne Bezeichner)
 Deklaration der Funktion durch Angabe des Prototyps
 double pow(double,int);
 Aufruf der Funktion (Prototyp muss bekannt sein)
x = pow(3.0,3); /* Liefert den Wert 27 */
 (Spätere) Implementierung der Funktion:
double pow(double x, int b) {
double erg = 1;
int i;
for (i = 0; i < b; i++)
erg *= x;
return erg;
}
156
Parameter von main

 Die Funktion main kann optional mit folgenden Parametern


verwendet werden:
 int main(int argc, char *argv[])
 argc ist dabei die dabei die Größe des Arrays argv
 argv ist ein Array auf Zeichenketten (vom Typ (char *)).

 Argumente von main werden beim Start des Programms von


der Kommandozeile automatisch mit den Zeichenketten der
Zeile gefüllt.
 Die erste Zeichenkette in argv ist also stets der Name des Programms
und es gilt argc > 0.
 Damit wird eine Parametrisierung des Hauptprogramms unterstützt.

157
Beispiel
#include <stdio.h>

int main(int argc, char *argv[]) {


int i;
for (i=1; i < argc; i++) {
printf(argv[i]);
printf((i < argc-1) ? " " : "\n");
}
}

Speicherbelegung nach Aufruf des


Programms mit folgender Kommandozeile:
echo Hallo C++ 0
argv echo\0

Hallo\0

0 C++\0
158
Lokale Variabeln

 In einer Prozedur können lokale Variablen deklariert werden,


die nur innerhalb der Prozedur zugreifbar sind. Man
unterscheidet dabei:
 Normale lokale Variablen (automatische Variablen)
 Statisch lokale Variablen
 Schlüsselwort static wird bei der Deklaration vorangestellt.
 Automatische Variablen, also solche ohne das Attribut static,
werden bei jedem Funktionsaufruf (wenn die Deklaration
abgearbeitet wird) angelegt und beim Verlassen der Funktion
wieder (automatisch) gelöscht.
 Weitere Details siehe folgende Folie.
 Statische Variablen werden nur einmal bei einem
Programmlauf angelegt.
 Wird eine statische Variable innerhalb einer Funktion deklariert, so
steht diese wieder beim nächsten Aufruf mit dem letzten Wert zur
Verfügung.

159
Automatische Variablen

 Automatische Variablen werden


 angelegt, wenn die entsprechende Deklaration abgearbeitet wird,
 und automatisch gelöscht, wenn ihr Gültigkeitsbereich verlassen
wird.
 Man unterscheidet zwischen
 lokalem Gültigkeitsbereich
Dieser entsteht durch
 Funktionsdefinitionen und
 ein Paar von geschweiften Klammern { }.
 und globalem Gültigkeitsbereich.
Dieser bezieht sich auf die komplette Übersetzungseinheit (Datei).
 Ein Bezeichner (z. B. einer Variablen oder Funktion) gilt in
diesem Bereich von der Stelle an, an der er definiert ist.

160
Beispiel für Variablen
Variable mit globalem
#include <stdio.h> Gültigkeitsbereich
int tutNix;
lokale static Variable.
int genLabel ( ){ Diese wird einmal mit 0
static int globLabel = 0; initialisiert und behält ihren Wert
globLabel++; von Aufruf zu Aufruf.
return globLabel;
}
Liefert der Reihe nach die
int main () { Werte: 1, 2, 3, 4
int test;
for (test = 1; test < 5; test++ )
printf("Ergebnis von genLabel:%d\n", genLabel());
}

Globale Bezeichner
tutNix, genLabel, main
161
7.3 Verdeckte Bezeichner

 Die Deklaration einer lokalen Variablen gilt nur innerhalb der unmittelbar
umgebenden geschweiften Klammern, genauer:
 Von der Definitionsstelle an.
 Ineinander geschachtelte Deklarationen verdecken sich ggf.
 In einem Gültigkeitsbereich hat ein Bezeichner niemals zwei Bedeutungen.
#include <stdlib.h>

globale Definition von n


int n = 42;
int main (){
int n= 9;
{ int n = 12; lokale Definitionen von n, die
{ int n = 13;
sich gegenseitig verdecken
{ int n= 14;
printf("%d\n",n); Die Ausgabe von n bezieht sich
} auf das lokalste n also 14.
}
}
}
162
Klammergebirge
{
{
int n; n gilt nur in diesem Bereich.
}
}
 Die Deklaration einer lokalen Variablen gilt dann, wenn die entsprechende
Deklarationsanweisung ausgeführt wurde.
 Sie ist dann ab dieser Stelle in dem Block gültig.
 In einem Unterblock wird Sie dann ungültig, wenn wiederum eine Deklarationsanweisung
mit dem gleichen Bezeichner ausgeführt wird.
 Folgende Beispiele sind deshalb ok:

{ int n = 9; { int n = 9;
{ { int a = 5 + n;
printf("%d", n); /* 9 */ if (a < 10)
int n = 12; { int n = 12; }
printf("%d", n); /* 12 */ printf("%d", n); /* ? */
} }
} }
163
7.4 Prototyping - Funktionsdeklaration
 Wird der Anweisungsteil einer Funktion durch ein Semikolon
ersetzt, so sprechen wir von einer Funktionsdeklaration oder
einem Prototyp.
 Verwendung einer Funktion (Aufruf) im weiteren Programmteil.
 Implementierung der Funktion zu einem späteren Zeitpunkt.
 Funktionen müssen vor ihrer Verwendung entweder deklariert
oder implementiert sein.
Beispiel:
#include <stdio.h>

double sqr(double x);


x kann auch noch bei
ínt main(void) { der Deklaration
printf("%f", sqr(5)); weggelassen werden
}
double sqr(double x){ return x*x; }
164
Funktionstypen
 Funktionen lassen sich in ähnlicher Weise wie Variablen
deklarieren.
 Durch folgende Deklaration
double sqr(double), tmp;
wird eine Funktion sqr mit Ergebnistyp double und eine double-
Variable tmp deklariert.
 Eine Implementierung einer Funktion kann aber nicht innerhalb einer
anderen Funktion liegen.
 Funktionen haben einen globalen Gültigkeitsbereich.
 Funktionstypen
 Durch die Anweisungen
typedef double DoubleFunc(double);
DoubleFunc sqr;
wird zunächst ein neuer Typ DoubleFunc deklariert und dann ein
Bezeichner für eine Funktion deklariert, wobei deren Implementierung
später noch folgen kann.

165
Funktionsvariablen

 Man kann sogar „echte“ Funktionsvariablen deklarieren und


diesen Variablen passende Funktionen zuweisen.
 Hierbei ist zu beachten: der Wert einer Funktion ist eine Adresse!
(ähnlich wie bei einem Array).
 Eine konkrete Funktion passt zu einer Funktionsvariable, wenn
 Rückgabetypen gleich sind
 Typen in der Parameterliste paarweise gleich sind
 Folgende Anweisungen sind somit möglich:
/* PDoubleFunc ein Pointer auf eine Funktion */
DoubleFunc *x = sqr; /* ist eine legale Zuweisung */
x(4.0); /* liefert den Wert 16 */
 Alternative dazu:
typedef double (*PDoubleFunc)(double);
PDoubleFunc y = sqr;
y(4.0);

166
Nachtrag: Der Datentyp void

 Dieser Datentyp dient in C noch zur Unterscheidung zwischen


Prozeduren und Funktionen. Erstere werden durch den
„Rückgabetyp“ void gekennzeichnet.
 In C dient void außerdem zum generischen Programmieren.
Folgende Anweisungen sind z. B. erlaubt:
 Deklaration einer Pointervariable vom Typ void:
void * p;
 Zuweisung eines beliebigen Pointerwertes an die Variable p:
int *ip; oder double *dp;
p = ip; p = dp;
 Zuweisung eines Wertes vom Typ (void *) an eine andere
Pointervariable (in C auch ohne einen Cast möglich):
ip = (int *) p; oder dp = (double *) p;

 Häufig werden void-Typen bei sehr allgemeinen Funktionen


benutzt, wie dies im folgenden Anwendungsbeispiel der Fall
ist.

167
Anwendungsbeispiel (1)

 Eines der häufigsten Probleme der Informatik besteht aus dem


Sortieren einer Folge von Daten.
 Sortierkriterium der Daten soll vom Anwender festgelegt werden, indem
er eine Ordnungsfunktion mitliefert.
 Hierzu wird in einer Bibliothek eine Funktion qsort mit
folgendem Kopf zur Verfügung gestellt:
void qsort(
void *base, /* Datentyp der Elemente*/
size_t num, /* size_t: Rückgabetyp von sizeof() */
size_t width, /* size_t entspricht unsigned int */
int (*compare )(const void *, const void *)
);
 Es stellt sich dabei sofort die Frage, wie man die Funktion
überhaupt anwenden kann.
 Man beachte dabei, dass als vierter Parameter ein Zeiger auf eine
Funktion erwartet wird.

168
Anwendungsbeispiel (2)

#include <stdio.h>
#include <stdlib.h>

int compare( const void *arg1, const void *arg2 ) {


/* Vergleichsfunktion für Integer */
return *(int *) arg1 - *(int *) arg2;
}

int main(void) {
const int MAX = 100;
int i, j, arr[MAX], *ip = arr;
/* Initialisierung des Arrays mit zufälligen Werten */
for (j = 0; j < MAX; j++)
arr[j] = rand() % 1000;
/* Aufruf der Funktion mit den entsprechenden Parametern */
qsort(ip, (size_t) MAX, sizeof(int), compare);
for(i = 0; i < MAX; ++i )
printf("%d %d\n", i, arr[i]);
}
169
7.5 Inline

 Funktionen können mit dem Schlüsselwort inline deklariert sein.


Dies gibt dem C-Compiler den Hinweis, den Funktionsaufruf zu
vermeiden.
 Der Funktionsaufruf wird (wenn möglich) durch den "Funktionsrumpf"
ersetzt. Der Funktionsaufruf wird also nach Möglichkeit eingespart.
 Beispiel:
 Wurde inline int fak(int i) {...} definiert, kann ein guter
Compiler fak(5) bereits durch 120 ersetzen.

170
Beispiel für Inline

#include <stdio.h>

inline double sqr(double x){


return x * x;
}

inline double cube(double x){


return x * x * x;
}

int main(void){
double x =5;

printf("square of %f = %f \ncube of %f = %f\n",


x, sqr(x), x, cube(x));
}

171
7.6 Rekursion

 Natürlich können Unterprogramme auch in C rekursiv sein.


Die Funktion kann sich in ihrem Rumpf selbst aufrufen.

 Die rekursive Fassung der Fakultätsfunktion lautet:

int fak (int n) {return n<2 ? 1 : n*fak(n-1);}

172
7.7 Standardfunktionen

 Funktionalität, die in vielen Programmen verwendet werden


kann, wird durch Bibliotheken zur Verfügung gestellt.
 Zum Aufruf der Funktion muss zumindest der Prototyp der
Funktion durch Import einer Header-Datei bekannt sein.
 Typische Bibliotheken
 strings.h
 Funktionen auf Zeichenketten wie z. B. strgcmp
 stdlib.h
 Wichtige Algorithmen und Datenstrukturen wie z. B. qsort,
drand48
 Speicherverwaltung wie z. B. malloc, free
 stdio.h
 Funktionalität für das Lesen und Schreiben auf Dateien wie
z. B. open, read, write

173
Beispiel

#include <stdio.h>
#include <string.h>

int stringLaenge(char *str){


int l = 0;
char *s = str;
while (*s++) l++;
return l;
}

int main (void){


char test[] = "Dies ist ein Beispiel.";
/* Str.Länge == 22 */
printf(" Str.Länge : %d\n", stringLaenge(test));
printf(" Str.Länge : %d\n", strlen(test));
}

174
Stringprozeduren aus der Bibliothek
string.h

strlen liefert die Stringlänge.

strtok String Tokenizer

strcat Aneinanderhängen zweier Strings: Concatenate

strcmp Vergleich zweier Strings: Compare

strchr Finde das erste Auftreten eines char in einem String.

strcpy Kopieren eines Strings: Copy

....... und viele mehr ....


175
Zeitmessung

 Mit Hilfe von


#include <time.h> können Prozeduren und
Datentypen zur Zeitmessung benutzt werden:

 time_t und clock_t sind die notwendigen Datentypen.


 Diese entsprechen dem Typ unsigned int.
 Funktion time_t time (time_t*) liefert die aktuelle
Prozesszeit in Sekunden.
 Funktion clock_t clock (void) liefert die aktuelle
Prozesszeit in Millisekunden.
 Genauer: in der Einheit CLOCKS_PER_SEC, die aber (zumindest
unter Linux) 1000 ist.

176
Zeitmessung - Beispiel

#include <stdio.h>
#include <time.h>

void ausgabe (int x) { printf("%d \n",x); }

int main (int argc, char *argv[]) {


time_t zeitA, zeit;
clock_t clA, cl;

clA = clock(); time(&zeitA);


/* Übungsaufgabe: Implementierung von yourSort */
yourSort(argv[1]);
cl = clock(); time(&zeit);

ausgabe((int)(zeit – zeitA));
ausgabe((int)(cl – clA));
ausgabe(CLOCKS_PER_SEC);
}
177
8. Programmieren im Großen

 Übersicht
 Präprozessor
 Compiler
 Linker
 Source-Dateien, Header-Dateien
 Projekte
 Makefiles
 Debugger
 Versionierung
 Bibliotheken

178
Motivation (1)

 Programme sind oft sehr umfangreich


 Große Anzahl von Codezeilen
 Komplexe Funktionalität
 Vielfältige Datentypen und Funktionen
 Um solche Programme noch überschaubar zu halten, sollte man
den Programmcode auf mehrere Übersetzungseinheiten verteilen.
 Ziel der Verteilung: Bildung logisch zusammengehörender Einheiten
 Solch eine Übersetzungseinheit enthält in der Regel keine
Funktion main.
 Beim prozeduralen Programmieren: Zusammenfassung von Code
mit verwandten Aufgabe wie z.B. Ein- und Ausgabe
 Beim objektorientierten Programmieren: Bereitstellung eines
selbstdefinierten Datentyps (im Rahmen der Möglichkeiten von C)
 Das eigentliche Programm entsteht erst, wenn einzeln übersetzte
Dateien durch den Linker verbunden werden, so dass diese Dateien
insgesamt:
 genau eine Funktion main
 zu jeder externen Variablen und Funktion sowie jedem Typ
genau eine Definition
enthalten.
179
Übersicht

 Die Umsetzung der Quelltexte (engl.: Sourcecode), d.h. der


Textdateien mit dem Programmtext, in einen Programmcode, d.h.
in Binärdateien, die von einem Computer ausführbar sind, erfolgt
in C/C++ in drei Schritten:
 Der Präprozessor sucht in den Quelltexten nach speziellen Befehlen, um
dann im wesentlichen textuelle Ersetzungen durchzuführen.
 Der Compiler wandelt den so geänderten Code in sogenannten
Objektcode um, der aber noch offene Aufrufe enthält (d.h. es erfolgt noch
keine Zuweisung eines Funktionsaufrufs zu einer
Funktionsimplementierung).
 Der Linker verbindet die noch offenen Funktionsaufrufe mit den
zugehörigen Funktionsimplementierungen.

180
Präprozessor

 Der Präprozessor bearbeitet die Quelltexte zuerst.


 Dabei sucht er nach speziellen Anweisungen, die mit einem
# - Zeichen gekennzeichnet sind.
 Die # - Anweisungen haben hauptsächlich folgende Aufgaben:
 Dateien in andere einfügen (#include)
 Konstanten definieren und ersetzen (#define)
 Makros zur Verfügung stellen (#define)
 Teile des Quelltextes während des Compilierens selektieren
(#ifdef / #ifndef) "Bedingte Compilation".

181
Compiler

 Der Compiler bekommt den vom Präprozessor überarbeiteten


Quelltext und setzt ihn in einen sogenannten Objektcode um.
 Erkennen und Melden lexikalischer, syntaktischer (grammatischer)
und semantischer Fehler.
 Erzeugen des Objektcodes (wenn keine Fehler mehr auftreten).
 Dieser Objektcode ist schon ein maschinennaher Code, der aber
noch nicht ausgeführt werden kann, da er noch unaufgelöste
Externverweise enthalten kann.
 An den Stellen, an denen ein Externverweis benutzt werden soll,
steht nur ein entsprechender Hinweis im Objektcode, der später
noch vervollständigt werden muss.
 Der Objektcode ist somit nur ein Zwischencode.
 Viele Systeme speichern den Objektcode in separate Dateien, die
 unter Linux mit der Endung „.o“,
 unter Windows mit der Endung „.obj“ gekennzeichnet sind.
 Compiler
 unter Linux: g++ -c
 unter Windows: cl.exe -c
183
Linker

 Der letzte Schritt bei der Generierung eines Programms ist


das Linken (auch Binden oder Montieren genannt).
 Aufgabe des Linkers:
 Zusammensetzen (ggf. von verschiedenen) Compilern generierte
Objektcode-Dateien zu einem vollständigen Programm zusammen.
 Bei Änderung einer Objektdatei muss der Linker erneut gestartet
werden (siehe auch Makefiles)
 Dabei sucht der Linker nach Stellen, die Externverweise
benutzen, und nach Stellen, in denen diese Externverweise
definiert sind.
 Falls mehrere Definitionen existieren, gibt der Linker eine
Fehlermeldung aus.
 Linker
 unter Linux: ld
 unter Windows: link.exe

184
Deklaration und Definition

 Problem: Speicherreservierung bei Programmen mit mehreren


Übersetzungseinheiten
 Unterscheidung zwischen
 Deklaration eines Bezeichners
 Zuordnung eines Typs zu einem Bezeichner
 Verwendung des Schlüsselworts extern
 Eine Variable der Speicherklasse static darf nicht extern verwendet
werden.
 keine Reservierung von Speicherplatz
 keine Initialisierung
 Beliebige Wiederholung der Deklaration in einem Programm möglich
 Definition einer Variable
 Zuordnung eines Typs zu einem Bezeichner
 Ohne Verwendung des Schlüsselworts extern
 Reservierung von Speicherplatz
 Keine Wiederholung der gleichen Definition in einem Programm
 Beispiel
extern int i; /* Deklaration, beliebig oft */
int i = 0; /* Definition, nur einmal */

185
Header-Dateien (1)

 Traditionell werden C bzw. C++ Programme in Header- und Source-


Dateien aufgeteilt.
 Header-Dateien sollten im Wesentlichen nur Deklarationen und Source-
Dateien die Implementierung enthalten.
 Vorschriften für die Aufteilung des Programmtextes auf diese Dateien gibt es
nicht. Alles ist möglich!
 Header-Dateien werden mit der #include-Anweisung in andere
Dateien eingebunden.
 Eine Übersetzungseinheit besteht daher i. A. aus einer Source-Datei und
zusätzlich aus verschiedenen Header-Dateien.
 Durch die #include-Anweisungen entsteht aus einer Source-Datei zusammen
mit den inkludierten Header-Dateien eine expandierte Datei:
 Dies ist genau das was der Compiler zu lesen bekommt!
 Diese expandierte Datei muss syntaktisch korrekt sein.

186
Header-Dateien (2)

 In Header-Dateien sollten nur "Schnittstelleninformationen" sein.


 Prototypen von Funktionen
 Konstantendefinitionen
 Typdefinitionen
 Deklarationen von globalen Variablen
 Die Implementierung von Funktionen sollte in Source-Dateien sein.
 Bei der Aufteilung in Source- und Header-Dateien gibt es einige wichtige
Grundregeln zu beachten:
 Zu einer Source-Datei sollte eine zugehörige Header-Datei definiert werden,
die alle Definitionen enthält, die nicht lokal sind. Nur Informationen, die von
anderen Programmteilen benötigt werden, gehören in eine Header-Datei.
 Alles andere gehört in die Source-Datei.
 Man sollte möglichst viele Informationen in zusätzliche Header-Dateien
einbringen bzw. solche nutzen, um gemeinsame Definitionen
auszuklammern und an einer Stelle zu konzentrieren.

187
Header-Dateien (3)

 Probleme
 Wenn man viele Header-Dateien benutzt, läuft man Gefahr, einige
Programmteile doppelt zu compilieren.
 Doppelte Headerdateien können auch zu einem Fehler beim Compilieren
führen.
 Vor einer zweiten Compilierung kann man sich mit bedingter
Compilation schützen:

#ifndef Dateikennung
#define Dateikennung
// Code
#endif

188
Projekte
Abhängigkeiten: Ergebnisse:

Datei1.h Datei1.c Datei1.o


Datei2.h Datei2.c Datei2.o

... ... ...

... Dateien.c Datein.o

Dateim.h
189
Projekt.exe
Make: Übersetzen großer Programme (1)

 make ist ein Programm zur Verwaltung großer Programme, die aus
kleinen Einheiten (Source-Dateien, Header-Dateien und
Objektdateien ) bestehen.
 make ist nicht nur anwendbar für C/C++, sondern kann flexibel für alle
Sprachen (und andere Software) eingesetzt werden.
 make ist im UNIX-Umfeld entwickelt worden.
 Zerlegung des Quellcodes in kleinere Einheiten hat folgende
Vorteile
 Kleine, übersichtliche Übersetzungseinheiten
 Bereitstellung gemeinsam genutzter Deklarationen in Header-Dateien
 Aber auch folgenden Nachteil (?):
 Die Änderung einer Einheit macht eine Neuübersetzung anderer Einheiten
erforderlich.

190
Make: Übersetzen großer Programme (2)

 Lösungsansätze
 Neuübersetzung aller beteiligten Dateien
 Funktioniert zwar, führt aber zu sehr langen Übersetzungszeiten.
 Neuübersetzung nur der relevanten Dateien
 make hilft dabei diese Dateien automatisch zu finden und ihre
Übersetzung auszulösen.

191
Programme als Graphen (1)

 Ein C/C++-Programm kann als Graph dargestellt werden, wobei


die Knoten wie folgt definiert sind:
 Wurzel: Zielprogramm,
 inneren Knoten: Objektdateien,
 Blätter: Quelldateien.
Es gibt eine Kante zwischen zwei Knoten, falls
 eine Abhängigkeit bei der Übersetzung bzw. beim Linken besteht.
 Beispiel:
 target.o hängt direkt ab von
target.c und global.h
 help.o hängt direkt ab von
global.h und help.c target

 target hängt direkt ab


von target.o und help.o target.o help.o

target.c global.h help.c

192
Programme als Graphen (2)

 In jedem Knoten des Graphen wird noch der Zeitpunkt der


letzten Änderung der Datei abgelegt (Alter).
 Ein Knoten K ist dann aktuell, wenn alle seine Kindknoten aktuell sind
und diese älter als K sind.
 Ist dies nicht der Fall, muss eine Aktion (Neuübersetzung, Linken etc.)
der Kinder ausgelöst werden, welche die obere Eigenschaft verletzen.
 Wichtig dabei ist, dass keine zyklischen Abhängigkeiten
zwischen Dateien bestehen!
 Beispiel:
 Änderung von help.c führt
 Neuübersetzung von help.c target

 Linken von target


 ABER keine Neuübersetzung target.o help.o

von target.c
target.c global.h help.c

193
Erstellung eines Makefiles

 make bekommt als Eingabe den Abhängigkeitsgraphen und


überprüft, ob das Zielprogramm aktuell ist.
 Hierzu muss der Benutzer die Graphstruktur in eine Textdatei (dem
sogenannten Makefile) übertragen.
 Gibt man make keinen Dateinamen, so sucht make nach einer
Textdatei mit Namen „Makefile“.

194
Bestandteile von make-Dateien

 Macro-Definitionen
 Globale Variablen, die textuell durch ihren Wert ersetzt
werden.
 Explizite Regeln
 Abbildung eines (oder mehrere) Knotens K und seiner Kinder.
 Definition von Aktionen zur Aktualisierung des Knotens K
 Implizite Regeln
 Abbildung einer Gruppe von Knoten, die jeweils nur von
einem Kind abhängen.
 Definition von Aktionen zur Aktualisierung der Knoten.
 Eine implizite Regel wird von make nur verwendet, wenn für eine zu
erzeugende Datei keine explizite Regel vorhanden ist.

195
Beispiel

#Definition von Macros


OBJECTS= main.o input.o buf.o
LIBS= -lX11
# Regeln
edit: $(OBJECTS)
g++ -o edit $(OBJECTS) $(LIBS)
main.o: main.cc defs.hh buf.hh
g++ -c main.c
input.o: input.cc defs.hh
g++ -c input.c
buf.o: buf.cc buf.hh defs.hh
g++ -c buf.c
clean:
rm edit $(OBJECTS) core

196
Macros

 Über Macros können globale Bezeichner im Makefile definiert werden.


 Eine Macrodeklaration besteht aus
 Namen (Konvention: Großbuchstaben),
 Zuweisungssymbol,
 und einem Wert.
 Beispiel:
OBJS=target.o help.o
 Benutzen von Macros
 Voranstellen von $ vor dem geklammerten Macro-Namen
 Beispiel:
target: $(OBJS)

197
Explizite Regeln
 Folgende Formate werden unterstützt:
 <parent>: <child1> <child2> <child3> ...; <cmd1>
[TAB]<cmd2>
[TAB]<cmd3>
...
 <parent>: <child1> <child2> <child3> ...
[TAB]<cmd1>
[TAB]<cmd2>
...
 Abhängigkeiten zwischen mehreren Elternknoten und den gleichen Kindern:
<parent1> <parent2>: <child1> <child2> <child3> ...
[TAB]<cmd1>
[TAB]<cmd2>
...
 Häufige Fehlerquelle: fehlende [TAB]-Zeichen, Leerzeichen
 Beispiel:
target.o: target.c global.h
[TAB]$(CC) $(CCOPTIONS) target.c

198
Implizite Regeln

 Abhängigkeit einer Gruppe von Knotenpaaren, die aus einem Kind


und einem Elternknoten bestehen.
 Gruppen werden über die Dateiextension identifiziert.
 Befehlsformat (Suffixregel):
 .<ext1>.<ext2>:
[TAB]<cmd1>
...
 Verwendung folgender Macros bei den Aktionen
 $<: Dateiname mit Extension <ext1> (Kindknoten)
 $@: Dateiname mit Extension <ext2> (Elternknoten)
 Beispiel:
 .c.o:
[TAB]$(CC) $(CCOPTIONS) $<

199
Weitere Syntax für Makefiles
 Makefiles sind zeilenorientiert. Möchte man zwei Zeilen miteinander
verbinden, muss am Ende der ersten Zeile ein „\“ angefügt werden.
 Kommentarzeilen: Zeichen „#“ am Anfang der Zeile
 Voranstellen des Zeichens „.“ vor einer Aktion führt dazu, dass bei
einem Fehler in der Aktion das Makefile weiterverarbeitet wird.
Ansonsten führt dies zum Abbruch von make.
 Voranstellen des Zeichens „@“ führt dazu, dass der Befehl nicht
ausgegeben wird.
 Es können mehrere Wurzelknoten im Graphen existieren.
 Beim Aufruf kann der gewünschte Wurzelknoten angegeben werden.
 Statt eine Wurzel mit einer Datei zu assoziieren, können auch
imaginäre Ziele vereinbart werden.
 Beispiel:
clean: ; rm –f *.o
 Weitere Anwendung ist z. B. die Erzeugung einer zip-Datei, die alle
Softwarequellen beinhaltet.

200
Starten von make
 Unter Visual C++
 nmake [options] [targets]
 Unter Unix
 make [options] [targets]
 gmake [options] [targets]
 Optionen (siehe auch Dokumentation)
 -f <dateiname>
Wird diese Option angegeben, wird die Datei mit diesem Namen als
Makefile benutzt. Ansonsten such make/nmake nach einer Datei mit
dem Namen Makefile.
 Automatische Erstellung von Makefiles durch spezielle
Programme (innerhalb von Entwicklungsumgebungen)

201
Debugger
 Ziel eines Debuggers:
 Unterstützung der Fehlersuche in ausführbaren Programmen.
 Voraussetzung für das Debuggen
 Programmübersetzung mit der Debug-Option
 Funktionalität eines Debuggers
 Definition von Breakpoints (Haltepunkten)
 mit oder ohne Bedingung
 Durchlaufen des Programms bis zum
 Nächsten Haltepunkt
 Nächsten Befehl
 Ausgabe des lokalen Speichers (Stack) der Funktionen
 Beobachtung von Variablen und Speicherbereichen

202
Versionskontrolle

 Für die Programmerstellung im Team wird meist ein Werkzeug zur


Konsistenzsicherung der Quelldateien benötigt. Aufgabe:
 Definition einer aktuellen Projektversion
 Erkennen und Auflösen von Konflikten
 Alle Quelldateien werden in einer Datenbank zentral verwaltet.
 Optimistische Variante
 Alle Benutzer können Quelldateien editieren.
 Beim Zurückschreiben der Daten wird erkannt, ob es einen Konflikt
mit anderen Benutzern gibt.
 Konflikte werden mittels Rücksprache mit anderen behoben.
 Pessimistische Variante
 Beim Lesen der Datei aus der Datenbank wird die Datei für andere
Benutzer gesperrt, bis die Datei wieder vom Benutzer frei gegeben
wird.
 Nachteil: Grad der parallelen Programmentwicklung wird
eingeschränkt!

203
Bibliotheken

 Eine Programmbibliothek ist eine Sammlung von


Objektdateien, die unter einem Namen angesprochen werden
können.
 Bibliotheken gibt es in drei Varianten:
 Statische Bibliotheken (static libraries)
 Diese werden bereits zur Übersetzungszeit (genauer gesagt
beim Linken) komplett zum Hauptprogramm gebunden.
 Gemeinsam genutzte Bibliotheken (shared libraries)
 Ähnlich wie statische Bibliotheken, aber im Hauptprogramm
befindet sich nur eine Referenz auf die zentral verwaltete
Bibliothek.
 Dynamische Bibliotheken (dynamic libraries)
 Diese werden dynamisch zur Laufzeit des Programms
gebunden.

204
Statische Bibliotheken

 Eine statische Bibliothek ist im Wesentlichen ein Archiv, in


dem alle zugehörigen Objektdateien liegen.
 Unter Linux haben die statischen Bibliotheken das Suffix ".a".
 Das Archiv wird mit dem Befehl ar erstellt.
 Die Programmbibliotheken besitzen das Präfix "lib".
 Beispiel
 Erstellen eines Archivs
ar rcs libEx.a Ex1.o Ex2.o
 Nutzen eines Archivs durch Weglassen des Präfix lib
gcc Hallo.c –L. –lEx
 Typischerweise gibt es bei Betriebssystemen noch spezielle
Befehle, um Archive nach Symbolen zu durchsuchen.
 Unter Linux z. B. das Kommando nm:
nm -o /usr/lib/*.a | grep drand48

205
Gemeinsam genutzte Bibliotheken

 Eine gemeinsam genutzte Bibliothek ist ebenfalls ein Archiv,


das gemeinsam von mehreren Programmen genutzt wird.
 Unter Linux haben diese Bibliotheken den Suffix ".so".
 Verwendung gemeinsam genutzter Bibliotheken
 Explizite Angabe beim Linken mit der Option –l
 Implizite Angabe durch Anlegen eines Link-Pfads
 Erstellen gemeinsam genutzter Bibliotheken
 Kompilieren mit der Option fPIC
gcc –fPIC –c Ex1.c
gcc –fPIC –c Ex2.c
 Linken der Objektdateien
gcc -shared -Wl,-soname,libEx.so.l -o libEx.so.1.0
Ex1.o Ex2.o
Unter Linux gibt es Versionsnummern für die Bibliotheken.
 Vermeidung inkompatibler Bibliotheken
206
Weitere Schritte

 Setzen des Links auf die richtige Version


 ln –sf libEx.so.1.0 libEx.so
 Annahme: libEx.so liegt im lokalen Verzeichnis

Nutzen der Bibliothek


 Übersetzen des Programms
 gcc –c demo.c
 Binden des Programms
 gcc –o demo –L. –lEx
 Die Option –L zeigt dabei an, in welchem Verzeichnis nach
Bibliotheken gesucht wird.
 Durch die Option –l wird die Biblothek angesprochen. Wichtig ist
dabei, den Präfix lib wegzulassen.

207
Dynamische Bibliotheken

 Bei einer dynamischen Bibliothek (DL) wird nach Bedarf der


Programmcode aus dem Archiv geladen.
 Unter Linux ist das Archiv eine gemeinsam genutzte Bibliothek!
 Erstellung der Bibliothek wie bereits zuvor erläutert.
 Unter Windows haben die dynamischen Bibliotheken den Suffix
".dll". Man spricht dann auch von so genannten DLLs.

 Vorteile dynamischer Bibliotheken


 Kleinerer Umfang des ausführbaren Programms
 Binden nur der tatsächlich benötigten Teile der Bibliothek
 Geringerer Aufwand beim Linken des Programms
 Nachteil dynamischer Bibliotheken
 Dynamisches Binden erhöht die Laufzeit eines Programms

208
Benutzen dynamischer Bibliotheken

 Unter Verwendung einer API kann der Code aus dem Archiv
geladen werden.
 Vorgehensweise unter LINUX
 Hierzu muss <dlfcn.h> importiert werden.
 Durch den Aufruf der Funktion dlopen wird ein Archiv geöffnet und
bei Erfolg eine Referenz auf die Bibliothek geliefert.
 Danach kann mit der Funktion dlsym durch Angabe eines
symbolischen Namens die gewünschten Programmteile aus der
Bibliothek extrahiert werden.

209
Beispiel

 Nehmen wir an, dass die folgende Funktion halloLib in der


Bibliothek libHalloDL.so liegt.
void hello(void) {
printf("Hallo Dynamic Library/n");
}
 Folgendes Programm könnte jetzt die Funktion dynamisch
laden. Aus Gründen der Übersicht wurde auf das Abfangen
von Fehlern verzichtet.
void *module;
void (*demoFunc)(void);
module = dlopen("libHalloDL.so",RTLD_LAZY);
demoFunc = dlsym(module, "hello");
demoFunc();
dlclose(module);
 Übrigens müssen die Funktionen der Bibliotheken nicht
notwendigerweise in C implementiert worden sein.

210
What next?

 Die wichtigsten Schlüsselworte in C sind jetzt bekannt!


 Es gibt aber noch weitere, weniger gebräuchliche
Schlüsselworte:

auto double int struct


break else long switch
case enum register typedef
char extern return union
const float short unsigned
continue for signed void
default goto sizeof volatile
do if static while

Weitere Schlüsselwörter von C99


_Bool _Complex _Imaginary inline
restrict
211
Selten gebrauchte Schlüsselwörter (1)

 auto
 Schlüsselwort, um Variablen eine lokale Lebensdauer
zuzuweisen, z.B. auto int i = 0;
 Wird äußerst selten verwendet, da standardmäßig alle nicht-
statischen/-globalen Variablen eine lokale Lebensdauer haben!
 register
 Zeigt dem Compiler an, dass die nachfolgend definierte
Variable – wenn möglich – in einem CPU-Register gespeichert
werden soll
Variable muss nicht ständig aus dem Speicher geladen werden
 Insbesondere bei häufig verwendeten Variablen (z.B.
Laufvariablen) sinnvoll:
register int i;
for(i=0; i<1000; i++){
...
}
212
Selten gebrauchte Schlüsselwörter (2)

 volatile
 Manchmal können Variablen durch Ereignisse manipuliert
werden, die sich dem Einfluss des Compilers entziehen, z.B.
durch:
 Zugriff mehrerer konkurrierender Threads auf dieselbe Variable.
 Verarbeitung eines Hardware-Interrupts

 Kennzeichnung solcher Variablen durch Schlüsselwort


volatile (= unbeständig, unberechenbar)
 Der Compiler darf für solche Variablen keine Optimierungen
anwenden (z.B. in Register halten), sondern muss immer den
tatsächlichen Zustand der Variablen im Speicher
berücksichtigen

213
Selten gebrauchte Schlüsselwörter (3)

 restrict (nur in C99, nicht C89, nicht C++ !)


 Kann nur in Verbindung mit Pointer-Variablen auftreten
 Programmierer „verspricht“ dem Compiler, dass alle Zugriffe
auf das Speicherobjekt, auf das der Pointer zeigt, einzig und
allein über diesen Pointer erfolgen.
 Ermöglicht dem Compiler Optimierung der Zugriffe
 Undefiniertes Verhalten, falls Kontrakt nicht eingehalten wird!

“A new feature of C99: The restrict type qualifier allows programs to


be written so that translators can produce significantly faster
executables. [...] Anyone for whom this is not a concern can safely
ignore this feature of the language.”
Quelle: Rationale for International Standard - Programming Languages - C
[std.dkuug.dk] (6.7.3.1 Formal definition of restrict)

214
9. Von C zu C++

 Kommentare
 In C++ können sowohl der klassische C-Kommentar als auch der
Zeilenkommentar verwendet werden.
 Klassischer C-Kommentar: /* Kommentar */
 Zeilenkommentar: // Zeilenkommentar

215
Typen in C++ (1)

 Typisierung
 C++ ist erheblich konsequenter was die Typüberprüfung angeht.
 Eine Zeigervariable kann nur ein Ausdruck eines spezielleren
Typs ohne Cast zugewiesen bekommen, aber nicht umgekehrt.
 Beispiel
void f1(void* p), f2(char* s);
f1 ("Funktioniert");
void *v;
f2(v); // Fehler !!
 Typkonvertierungen können folgendermaßen vorgenommen werden:
 int x = 42, y = 7;
float z = float(x) / y; // Das geht in C nicht!
float c = ((float) x)/y; // Geht auch!
 Inzwischen gibt es in C++ noch eine neuere Variante für die
Konvertierung!

216
Typen in C++ (2)

 Alle Typen, die mit struct, union und enum erzeugt wurden
können direkt über den Namen nach dem Typschlüsselwort
angesprochen werden.
 Beispiel
struct TelefonAdresse {
char *name;
char *vorwahl;
char *nummer;
}
TelefonAdresse meier;
 Anonyme union
 Ein union-Datentyp innerhalb eines struct-Datentyps benötigt keinen
Bezeichner.
 Fehler im Skript: wir hatten dies bereits als C-Feature
kennengelernt. Das war leider nicht richtig!

217
Datentyp bool

 Der Datentyp bool wird in C++ unterstützt.


 Vergleiche haben als Ergebnistyp bool
 Automatische Konvertierung nach int und umgekehrt
 bool b = 42;
 int i = b;
 Operationen auf bool
 Siehe Programmiersprache C

218
Deklarationen

 Deklarationen können wie in neueren Versionen von C an


einer beliebigen Stelle im Programm stehen.
 Lokale Variablendeklaration in einer for-Schleife
 for (int i=0; i < n; i++) sum += i;
 Die Gültigkeit der Variablen ist wie in Java auf den Block der
for-Schleife beschränkt.

219
Scope-Operator

 Problem
 Lokaler Bezeichner ist gleich einem globalen Bezeichner
 Dann ist im Block der lokalen Deklaration nur der lokale
Bezeichner ansprechbar.
 In C++ können auch verdeckte globale Bezeichner durch den
Scope-Operator "::" angesprochen werden.
 Beispiel
int xy = 42; // Deklaration globaler Bezeichner
int main(void) {
int xy = 7;
++::xy; // globaler Bezeichner
xy--; // lokaler Bezeichner
}

220
Referenz

 Der Zugriff auf eine Variable kann in C++ noch zusätzlich


über eine Referenz erfolgen.
 Eine Referenz ist keine eigenständige Variable, sondern nur ein
Alias für eine andere Variable oder einem Ausdruck mit L-Wert.
 Mit L-Wert bezeichnet man alles in C++, was links vom
Zuweisungsoperator stehen darf.
 Eine Referenz muss bei ihrer Deklaration initialisiert werden.
 Eine Neuzuordnung einer Referenz ist nicht möglich!
 Deklaration
int x;
int &a = x;
// a ist ein Alias für x und somit keine Wertzuweisung

221
Referenzen als Parameter

 Sinnvoll sind Referenzen eigentlich nur bei Methoden:


void swap(int &a, int &b){
int tmp = a;
a = b;
b = tmp;
}
 Der Aufruf von swap(x,y) bewirkt, dass die Werte
von x und y getauscht werden.
 Auch als Rückgabetyp einer Funktion sind
Referenzen durchaus sinnvoll.
 Man erspart sich dabei die explizite Übergabe von Adressen.
 Der Funktionsaufruf darf links vom Zuweisungsoperator
stehen.

222
Beispiel

const int MAX = 4;


struct DoubleArray {
double x[MAX];
};

DoubleArray& rotate(DoubleArray &a) {


double first = a.x[0];
for (int i = 1; i < MAX;i++)
a.x[i-1] = a.x[i];
a.x[MAX-1] = first;
return a;
}

 Möglicher Aufruf
rotate(rotate(d)).x[0] = 2.2;
// statt rotate(d); rotate(d); d.x[0] = 2.2;

223
Problem

 Typisches Laufzeitproblem in C++ ist die Initialisierung einer


Referenz mit einer lokalen Variable.
 Beispiel
int& f(){
int x = 42;
return x;
}

int t = f();
Da die Funktion f nur ein Alias für x liefert, x aber eine automatische
Variable ist, kann dies zu Problemen führen.
 Dies führt nur zu einer Warnung des Compilers, nicht zu einem
Übersetzungsfehler!
 Dies führt auch nicht direkt zum Abbruch des Programms!
 Die Variable verweist aber auf einen nicht belegten Speicherbereich!

224
Parameter mit Defaultwert

 C++ bietet die Möglichkeit, Parameter einer Funktion mit


einem Defaultwert zu belegen.
 Beim Aufruf der Funktion braucht der Parameter nicht zwingend
einen Ausdruck.
 Beispiel
 int ggt(int x, int y = 42);
 Aufruf der Funktion:
z = ggt(14); // Berechnung des ggt von 14 und 42
z = ggt(14, 22); // Berechnung des ggt von 14 und 22
 Einschränkungen
 Die mit Defaultwerten belegten Parameter stehen am Ende der
Parameterliste.
 Beim Aufruf der Funktion müssen immer die ersten k der n
Parameter einen Wert zugewiesen bekommen.
 Die restlichen n-k Parameter bekommen ihren Defaultwert

225
const (1)

 Das Schlüsselwort const wird genutzt zur Deklaration von


Konstanten.
 Bereits bei der Deklaration der Konstanten muss eine
Wertzuweisung erfolgen.
 Vorteil:
 Vermeidung von #define
 Diese Konstanten sind per Default statische Variablen.
 Mit extern kann dies wieder unterbunden werden.
 Zur Verwirrung tragen i. A. die mit const deklarierten
Pointervariablen bei.
 const char *p; // p ist ein Pointer auf ein const char
 p = "Einmal"; // Das geht in Ordnung
 p = "Nie"; // Das ist ein Fehler.

226
const (2)

 Deklarationen von konstanten Pointervariablen


int i;
int * const p = (int *) malloc(size(int));
*p = 42; // erlaubte Zuweisung
p = &i; // Fehler
 Deklaration von konstanten Pointervariablen, die auf
Konstanten verweisen.
const int laenge = 42;
const int const *p = &laenge;
Dadurch kann weder p noch *p verändert werden.
 Typische Anwendung von const: Parameter von Funktionen

227
Überladen von Funktionen

 Der gleiche Bezeichner darf in verschiedenen


Funktionsdeklarationen verwendet werden.
 Wie in Java müssen sich die Funktionen aber in Ihrer Signatur
unterscheiden!
 Zu der Signatur gehört neben dem Bezeichner noch die Liste der
Parametertypen.
 Der Rückgabetyp gehört nicht zur Signatur.
 Beispiel
void swap(int &x, int &y);
void swap(double &x, double &y);
 Beim Aufruf wird in Abhängigkeit der Typen der Parameter
die entsprechende Funktion gerufen.
 Hier kann es aber zu Mehrdeutigkeiten z. B. im Fall von
Defaultwerten kommen.

228
Dynamische Speicherreservierung

 In C++ gibt es für die Reservierung den new- und für die
Freigabe den delete-Operator.
double *f1 = new double;
double *f2 = new double(3.14);
delete f1; delete f2;
 Bei dynamischen Arrays muss etwas anders verfahren
werden.
 Speicherreservierung int * p = new int[42];
 Speicherrückgabe delete[] p;

 Die Speicherverwaltung mit malloc und free ist auch in


C++ möglich, aber...
 ein mit malloc reservierter Speicherbereich darf nicht mit delete
freigegeben werden und
 free darf nicht auf einen mit new reservierten Speicherbereich
angewendet werden! ( Sonst: Undefiniertes Verhalten!)
229
Klassen und Objekte

 Moduln und Klassen


 Vorgeschichte der Objektorientierung
 Prinzipien der Objektorientierung
 Objekte / Klassen in C++
 Felder und Methoden
 Datenkapselung: public / private / protected
 Deklaration und Implementierung
 Konstruktoren und Destruktoren
 Copy-Konstruktor
 Ausführliches Beispiel: CDatum
 static / const–Elemente von Klassen

230
Softwaremodule (Anforderungen 1)

 Unter einem Modul wollen wir zunächst einen Teil einer


Software verstehen, bei der folgende Anforderungen erfüllt
sind:
 Modulare Zerlegung:
Zerlegung eines Softwaresystems in kleinere Einheiten
 Komposition von Modulen:
flexible Kombination von Modulen zu neuen Softwarekomponenten
 Übersichtlichkeit und Verständnis eines Moduls:
ohne das gesamte System tatsächlich verstehen zu müssen
 Kontinuität:
Kleine Änderung in der Anforderung führt zu nur einer kleinen
Änderung eines Moduls
 Schutz vor Fehlern:
Fehler innerhalb eines Moduls sollen nur wenige Auswirkungen auf
andere Module besitzen.

231
Softwaremodule (Anforderungen 2)

 Module sollen syntaktische Einheiten in der


Programmiersprache sein.
 Unterstützung von Zerlegung und Komposition.
 Module sollten möglichst unabhängig voneinander sein
 Ziel: Gegenseitiger Zugriffsschutz
 Bei Abhängigkeit zweier Module sollte der Datenaustausch
möglichst niedrig sein.
 Der Informationsaustausch zwischen Moduln muss im
Programm offensichtlich sein.
 Alle Information innerhalb eines Moduls sind geschützt.

232
Realisierung von Softwaremoduln

 Wünschenswert sind Module, die sich erweitern lassen und


flexibel einsetzbar sind.
 Vor der objektorientierten Programmierung konnte dies nur
durch Nutzung von vorläufig undefinierten Datentypen und
Funktionen und von Header- und Include-Dateien realisiert
werden.
 Es fehlten Konzepte zum Zugriffsschutz.
 Im Gegensatz zur strukturierten Programmierung, bei der
vom Algorithmus und den davon abgeleiteten Prozessen
ausgegangen wird, werden bei der objektorientierten
Programmierung zunächst Datentypen, Datenstrukturen und
Objekte betrachtet.

233
Vorgeschichte

 Die Prinzipien der objektorientierten Programmierung waren


bereits Ende der 60er Jahre in der Programmiersprache
SIMULA realisiert worden.
 Ende der 70er Jahre wurde das Konzept konsequent in der
Sprache Smalltalk umgesetzt.
 Anfang der 80er Jahre wurde dann C++ entwickelt.
 Die objektorientierten Konzepte wurden dabei nicht so konsequent
wie in Smalltalk umgesetzt. Insbesondere aus Effizienzgründen und
der Kompatibilität zu C.

234
Wichtige Begriffe

 Klassen und Objekte (Datenkapselung)


 Zusammenfassung von Daten und Funktionen (Methoden)
 Konstruktion, Destruktion
 Vererbung
 Polymorphie

235
Objekte und Klassen in C++

 Klassen sind Strukturbeschreibungen, in denen Daten und


Funktionen gemeinsam vorkommen können.
 Durch eine Klassendefinition wird ein neuer Typ definiert (mit oder
ohne der Verwendung von typedef).
 Bei einer Variablendeklaration kann eine Klasse als Typ verwendet
werden.
 Dies ähnelt dem Konzept des struct-Datentyps.
 Klassen sind Fabriken für die Produktion neuer Objekte
 Objekte sind Werte, die zu einem Klassen-Datentyp gehören.
 Objekte werden implizit erzeugt oder können explizit mit dem new-
Operator erzeugt werden.
 Auf Objekte wird über eine Variable zugegriffen – direkt oder indirekt
über einen Pointer. Im letzteren Fall muss der benutzte
Speicherplatz explizit angelegt werden.
 Jedes Objekt ist eindeutig identifizierbar.

236
Klassen

 Objekte mit gleicher Struktur werden zu Objektmengen


zusammengefasst. Ihre Struktur wird in Form einer Klasse beschrieben.
Bestandteile einer Klasse
 Felder
 Datenteil eines Objekts
 Klasseninvarianten
 Bedingungen, die von allen Objekten der Klasse erfüllt werden.
 Methoden
 Beobachter keine Veränderung des Objektzustands
 Mutator Zustandsveränderung unter Berücksichtigung
der Invariante
 Konstruktor Speicherplatzreservierung mit ggf. einer Initialisierung
 Initialisierer Initialisierung eines Objekts
 Destruktor Speicherbereinigung und Rückgabe von Ressourcen

 Felder und Methoden einer Klasse werden an die davon abgeleiteten


Unterklassen weitervererbt.

237
Datenkapselung

 Trennung zwischen Schnittstelle und Implementierung


 Der Zustand eines Objekts, seine Felder sind ausschließlich über
die Methoden zugreifbar, die in der Schnittstelle deklariert werden.
 Dies gilt auch für Beobachter!
 Vorteile
 Klasseninvariante wird nicht verletzt.
 Unabhängigkeit von der Klassenimplementierung

Zustand

238
Informelle Einführung: Klassen in C++

 Eine Klasse ist eine logische Einheit (ein Modul) in einem


Programm, in dem
 Daten (Felder)
 und Methoden
definiert werden.
 Eine Klasse ähnelt zunächst einem struct-Datentyp, der
neben den Datenfeldern i. A. auch Funktionsvariablen
besitzt. class FeaturePoint{
int x;
 Beispiel: int y;
InitFunc f;
};

 Dies ist nahezu äquivalent zu dem entsprechenden struct-Datentyp.


 Man darf sogar in C++ das Schlüsselwort struct (statt class) benutzen.
Dies sollte aber nicht gemacht werden.

239
Zugriffsschutz

 Um die Felder und Methoden vor dem Zugriff von außen zu


schützen, gibt es für Klassen die Schlüsselwörter private,
public, protected.
 Alle Bezeichner, die nach public deklariert werden, können von
außen benutzt werden (analog zu struct-Datentypen).
 Alle Bezeichner, die nach dem Schlüsselwort private deklariert
werden, stehen nur zur internen Nutzung zur Verfügung.
 Wird keines der Schlüsselwörter verwendet, so sind alle Bezeichner
klassenintern zugreifbar.
 Beispiel
class FeaturePoint{
private: // kein Zugriff von außerhalb der Klasse möglich
int x;
int y;
public: // ab hier ist jeglicher Zugriff erlaubt
double value;
FeaturePoint(int i) { x = y = i;}
};
240
Zugriffsschutz

 Bei allen Bezeichner mit Zugriffsschutz private ist der direkte


Zugriff nur von Objekten aus der gleichen Klasse möglich.
 Im Gegensatz dazu sind Elemente mit dem Schlüsselwort public
auch von außen zugänglich.
 Auf Elemente mit dem Schlüsselwort protected kann man
zugreifen wie auf Elemente mit dem Schutz private.
 Zusätzlich kann man aber auch aus allen, von dieser Klasse
abgeleiteten Klassen, auf diese Elemente zugreifen.
 Abgeleitete Klassen werden später diskutiert.
 Die Schlüsselwörter private, public und protected kann man
zusammen mit einem Doppelpunkt an jeder Stelle in einer
Klassendefinition einstreuen.
 Auf diese Weise entstehen Abschnitte in denen jeweils der
gewünschte Zugriffsschutz gültig ist.

241
Vollständiges Beispiel

#include <iostream>
using namespace std; // Namensraum in C++

class FeaturePoint{
private: // kein Zugriff von außerhalb der Klasse möglich
int x;
int y;
public: // ab hier ist jeglicher Zugriff erlaubt
double value;
FeaturePoint(int i) { x = y = i; value = x*y; } // Konstruktor
int getX (){return x;}
int getY (){return y;}
};

int main(void){
FeaturePoint fp(5); // Variablendeklaration und Objekterzeugung
cout << fp.getX()<< " * " << fp.getY()<< " = " << fp.value << endl;
}

5 * 5 = 25
242
Datenausgabe in C++

 Um eine Ausgabe zu erzeugen, wird in C++ der Operator << und die
Variable std::cout benutzt.
 Voraussetzung: Inkludieren von iostream (ohne .h)
 std ist der Namensraum für cout. Durch explizite Angabe der Zeile "using
namespace std;" nach dem include kann auf das Präfix std verzichtet werden.
 Beim Operator << steht links davon die Variable des
Ausgabestroms und rechts die Zeichenkette, die ausgegeben wird.
 cout << "Hallo";
 Das Ergebnis des <<-Operators ist der linke Operand!
 Auch die Ausgabe anderer Datentypen ist in gleicher Weise
möglich.
 cout << 42;
 Der Operator kann auch mehrmals in einer Zeile angewendet
werden.
 cout << 42 << "Hallo";

243
Terminologie

 Die Daten einer Klasse werden in C++ Terminologie Daten-Elemente


genannt in anderen OO-Sprachen auch Attribute oder Felder.
 Wir werden im folgenden Text meist den Begriff Feld verwenden.
 Die Unterprogramme einer Klasse werden in C++ Terminologie Element-
Funktionen genannt.
 In anderen OO-Sprachen Programmiersprachen spricht man von
Methoden.
 Wir werden im folgenden Text den Begriff Methode verwenden.
 Den Begriff Elemente einer Klasse werden wir verwenden, wenn wir
Felder und/oder Methoden meinen.

244
Zugriff auf Felder und Methoden

 I.d.R. sollte der Zugriff auf Felder von Objekten implizit über Methoden
erfolgen.
 Der Zugriff auf Felder kann aber auch explizit erfolgen und zwar durch
Selektion mit einem Punkt. Der Zugriff auf Methoden erfolgt ebenso.
 Um von überall auf Felder und Methoden zugreifen zu können, müssen
diese public sein.

class XPDatum { XPDatum d1, d2;


public: d1.jahr = 2005;
int jahr; d1.monat = 11;
int monat;
d2.setJahr(2010);
int getJahr(){ return jahr;}
int getMonat(){ return monat;}
d2.setMonat(2);
void setJahr(int j){ jahr = j;} cout << d1.getJahr();
void setMonat(int m){ monat = m;} cout << d1.getMonat();
};

245
Beispiel einer Klasse
class CDatum {
int jahr;
Gekapselte int monat;
int tag;
Felder int stunde;
und int minute;
Methoden: void setAll(int j, int mo, int t, int s, int mi)
{ jahr = j; monat = mo;
tag = t; stunde = s; minute =mi;}
public:
CDatum(){ setAll(0,0,0,0,0);}
Öffentliche
Felder ~CDatum(){ }
und
int getJahr(){ return jahr;}
Methoden: int getMonat(){ return monat;}
// ...
void setJahr(int j){ jahr = j;}
void setMonat(int m){ monat = m;}
// ...
};

246
Implementierung von Methoden

 In dem letzten Beispiel ist der Anweisungsteil der Methoden jeweils


vollständig angegeben.
 Bei ähnlich kurzen Methoden empfiehlt sich die gleiche
Vorgehensweise, da diese Form der Definition von Methoden den
Compiler zur Optimierung der so definierten Funktionen veranlasst.
 Bei komplizierteren Methoden empfiehlt es sich jedoch, den
Anweisungsteil der Methoden nicht innerhalb der
Klassendeklaration anzugeben.
 Die Trennung der Klassendeklaration und der Implementierung
ihrer Methoden erhöht i. A. die Übersichtlichkeit von umfangreichen
Klassen.
 Die Implementierung kann später nachgeholt werden - und zwar in
derselben Datei oder (besser) in einer anderen Datei.
 Beim “Nachholen” eines Anweisungsteiles muss der Name der
Klasse und der Kopf der zu implementierenden Methoden wiederholt
werden.
247
Implementierung von Methoden: Beispiel
class CDatum {
// ...
Definition von void addMonat(int m);
Methoden: void zeige();
// ...
};

void CDatum::addMonat(int m) {
int neu = m + monat;
while (neu < 1) { jahr--; neu +=12; }
while (neu > 12) { jahr++; neu -=12; }
“Nachgeholte” monat = neu;
}
Implementierung der
Methoden: void CDatum::zeige() {
cout << "Jahr " << jahr
<< " Monat " << monat
<< " Tag " << tag
<< " Stunde " << stunde
<< " Minute " << minute
<< endl;
}
248
Konstruktoren und Destruktoren (1)

 Objekte werden mit einem Konstruktor initialisiert.


 Objekte werden mit einem Destruktor entsorgt, sobald sie nicht
mehr benötigt werden.
 In C++ benutzt man hierzu - wie in anderen Sprachen auch –
den Namen der Klasse:

class CDatum {
// ..
Konstruktor CDatum() { setAll(0,0,0,0,0);}
Destruktor ~CDatum(){ }
// ..
}

249
Konstruktoren und Destruktoren (2)

 Die Definition einer Klasse von Objekten kann mehrere


Konstruktoren aber nur einen Destruktor enthalten.
 Die Signaturen der Konstruktoren müssen dann unterschiedlich sein.
 Wenn vom Programmierer kein Konstruktor definiert wird, ergänzt
der C++ Compiler automatisch einen minimalen Konstruktor.
 Definiert der Programmierer einen Konstruktor, steht der minimale
Konstruktor nicht zur Verfügung!
 Wenn vom Programmierer kein Destruktor definiert wird, ergänzt
der C++ Compiler automatisch einen minimalen Destruktor.
 Konstruktoren und Destruktoren werden ebenso definiert,
wie alle anderen Methoden.
 Der Name des Konstruktors ist der der jeweiligen Klasse.
 Bei einem Destruktor ist noch ein ~ Zeichen vorangestellt.

250
Initialisierung

 In C++ müssen Felder in Konstruktoren initialisiert werden!


 In Java kann man schreiben:
class CDatum {
int jahr = 2007;
int monat = 11;
int tag = 11;
};

 In C++ geht das nicht! Stattdessen class CDatum {


muss man schreiben: int jahr;
 Wir werden später noch int monat;
int tag;
die „richtige“ Variante public:
CDatum(){
zum Initialisieren kennenlernen. jahr = 2007;
monat = 11;
tag = 11;
}
};
251
Beispiel mit mehreren Konstruktoren

Trivial - Konstruktor

CDatum(){ setAll(0,0,0,0,0);}
CDatum(int j, int mo, int t, int s, int mi){
setAll(j, mo, t, s, mi);
“Init” - Konstruktor
}
CDatum(const CDatum& d){
setAll(d.jahr, d.monat,
d.tag, d.stunde, d.minute);
}

Copy - Konstruktor

252
Copy-Konstruktoren (1)

 Ein Copy-Konstruktor initialisiert ein neues Objekt mit den Werten


eines bereits vorhandenen Objekts.
 Bei unserem Beispiel hat er keine spezielle Funktion.
 Ein Copy-Konstruktor verändert das kopierte Objekt nicht, daher
sollte er stets mit einem const-Parameter aufgerufen werden.
 Wenn kein eigener Copy-Konstruktor definiert wird, wird vom
Compiler automatisch ein minimaler Copy-Konstruktor definiert,
der einfach die entsprechenden Speicherbereiche kopiert "flache
Kopie".
 Der Copy-Konstruktor ist dann sinnvoll, wenn Strings oder
dynamische Datenstrukturen wie z.B. Listen initialisiert werden.
 Dabei kann erzwungen werden, dass man tatsächlich Kopien der
String- bzw. Listenelemente erhält und nicht etwa Kopien der Pointer
auf diese ("tiefe Kopie").

253
Copy-Konstruktoren (2)

 Für einen Klassentyp können Variable definiert werden, wie


für einfache Datentypen auch.
 Ihr Inhalt wird mit einem beliebigen Konstruktor initialisiert.
 Bei der Initialisierung einer Variablen durch eine andere wird
ein Copy-Konstruktor benutzt !
int main(void){
CDatum meinDatum(2005,8,22,15,13); Hier wird der Copy – Konstruktor
CDatum xDatum(meinDatum); explizit verwendet, um eine neue
CDatum yDatum = meinDatum; Variable vom Typ Datum zu
konstruieren.
xDatum.zeige();
yDatum.zeige(); Hier wird der Copy – Konstruktor
} implizit verwendet, um eine initiale
Zuweisung zu implementieren

Jahr 2005 Monat 8 Tag 22 Stunde 15 Minute 13


Jahr 2005 Monat 8 Tag 22 Stunde 15 Minute 13
254
Zuweisungen

 Die Zuweisung zwischen Variablen desselben Klassentyps


erfolgt bei der Initialisierung per Copy-Konstruktor.
 Zuweisungen, bei der keine Deklaration vorgenommen wird,
erfolgen mit dem üblichen Zuweisungsoperator.
int main(){ Beispiele für Zuweisungen
CDatum mDatum(2005,8,22,15,13); mit Pointervariablen
CDatum *pDatum = new CDatum(2005,9,1,11,11);
CDatum *qDatum = &mDatum;
CDatum nDatum = *pDatum;
mDatum.zeige();
Jahr 2005 Monat 8 Tag 22 Stunde 15 Minute 13
nDatum.zeige();
Jahr 2005 Monat 9 Tag 1 Stunde 11 Minute 11
nDatum.addMonat(2); Jahr 2005 Monat 9 Tag 1 Stunde 11 Minute 11
mDatum = *pDatum; Jahr 2005 Monat 11 Tag 1 Stunde 11 Minute 11
mDatum.zeige(); Jahr 2005 Monat 9 Tag 1 Stunde 11 Minute 11
nDatum.zeige();
mDatum = *qDatum;
mDatum.zeige();
}
255
Zuweisungen: C++ und Java

 Bei C++ gibt es normale Variablen, die Objekte eines


Klassentyps enthalten und Pointervariablen, die auf Objekte
eines Klassentyps zeigen.
 Bei den Zuweisungen müssen wir zwischen beiden Arten
von Variablen unterscheiden!
 Bei normalen Variablen wird der Inhalt bei einer Zuweisung kopiert.
 Bei einer Pointervariablen wird der Pointer kopiert!
 Bei Java gibt es grundsätzlich nur Pointervariablen, die auf
Objekte eines Klassentyps zeigen.
 Bei Zuweisungen werden also nur die Pointer (Referenzen) kopiert.

256
Konstruktor-Initialisierungs-Listen

 Konstruktoren benutzt man unter anderem, um den Feldern einer


Klasse einen vernünftigen Anfangswert zu geben.
 Um das zu vereinfachen, gibt es eine besondere Form der
Initialisierung der Felder mit einem Konstruktor:

Die Konstruktor-Initialisierungs-Liste.

 In dieser Liste können die Felder der Klasse “aufgerufen” werden, so als
ob sie eine Funktion zu ihrer eigenen Konstruktion wären. Dabei wird eine
Zuweisung vorgenommen.
 Die Reihenfolge der Zuweisungen erfolgt in der Reihenfolge der
Definition der Felder in der Klasse!
 Die Reihenfolge in der Initialisierungsliste ist dabei nicht relevant!
257
Beispiel mit einer Initialisierungs-Liste

Trivial - Konstruktor

CDatum() : jahr(0), monat(0), tag(0),


stunde(0), minute(0) {}

CDatum(int j, int mo, int t, int s, int mi)


: jahr(j), monat(mo), tag(t),
“Init” - Konstruktor stunde(s), minute(mi) {}

CDatum(const CDatum& d)
: jahr(d.jahr), monat(d.monat),
tag(d.tag), stunde(d.stunde),
minute(d.minute) {}

Copy - Konstruktor
258
Ein Hauptprogramm

 Wir wollen unsere Klasse CDatum nunmehr in einem


Hauptprogramm testen:

int main(){

CDatum nullDatum;
CDatum meinDatum(2005,8,12,15,13);
CDatum xDatum(meinDatum);

xDatum.zeige(); Jahr 2005 Monat 8 Tag 12 Stunde 15 Minute 13


nullDatum.zeige(); Jahr 0 Monat 0 Tag 0 Stunde 0 Minute 0
meinDatum.zeige(); Jahr 2005 Monat 8 Tag 12 Stunde 15 Minute 13

meinDatum.addMonat(15);
meinDatum.zeige(); Jahr 2006 Monat 11 Tag 12 Stunde 15 Minute 13
meinDatum.addMonat(-15);
meinDatum.zeige(); Jahr 2005 Monat 8 Tag 12 Stunde 15 Minute 13
meinDatum.addMonat(-15);
meinDatum.zeige(); Jahr 2004 Monat 5 Tag 12 Stunde 15 Minute 13
meinDatum.addMonat(15);
meinDatum.zeige(); Jahr 2005 Monat 8 Tag 12 Stunde 15 Minute 13
}

259
Datumsbeispiel: Verbesserung

 Die Implementierung der Klasse CDatum ist nicht ganz trivial.


 Dies zeigt sich bereits bei der Implementierung von addMonate.
 Erst recht gilt dies für andere Funktionen wie z.B. addTage.

 Alternative 1: Man nimmt keine Felder für Tage, ... sondern


ein einziges Feld für die “Zeit”.

Vorteil: addMonate, addTage, etc. werden einfacher.

Nachteil: Die Implementierung von getMonat, getTag,


etc. wird komplizierter.

 Alternative 2: Man kombiniert beides.

260
CDatum: Klassendefinition

class CDatum { Zeit in Minuten Zeit als Datum


long int zeit;
int jahr, monat, tag, stunde, minute;

void setAll(int j, int mo, int t, int s, int mi);


void berechneMinuten(); Datum Minuten
void berechneDatum();
Minuten Datum
public:
CDatum(){zeit = 0; berechneDatum();}
CDatum(int j, int mo, int t, int s, int mi){ setAll(j, mo, t, s, mi);}
CDatum(const CDatum& d){zeit = d.zeit; berechneDatum(); }
int getJahr() { return jahr;}
int getMonat() { return monat;}
int getTag() { return tag;}
int getStunde() { return stunde;}
int getMinute() { return minute;}
void addMinuten(int m = 1) {zeit += m; berechneDatum(); }
void addTage(int t = 1) { zeit += t*MIN_PER_TAG; berechneDatum(); }
void addWoche(int w = 1) { zeit += w*MIN_PER_WOCHE; berechneDatum(); }
void addMonate(int m = 1){ monat += m;berechneMinuten();berechneDatum();}
void zeige();
};

261
CDatum: Programmanfang (1)

#include <iostream>

const long int MIN_PER_STUNDE = 60;


const long int MIN_PER_TAG = 24*MIN_PER_STUNDE;
const long int MIN_PER_WOCHE = 7*MIN_PER_TAG;

bool schaltJahr( int jahr) {


if ( (jahr % 4) == 0 ) {
if ( (jahr % 100) == 0 ) {
if ( (jahr % 400) == 0 )
return true;
else
return false;
}
else
return true;
}
else
return false;
}

int tagePerJahr (int jahr) {


return schaltJahr(jahr) ? 366 : 365;
}

262
CDatum: Programmanfang (2)

long int minPerJahr ( int jahr) { return MIN_PER_TAG*tagePerJahr(jahr); }

int tagePerMonat ( int monat, int jahr) {


while ( monat > 12) { monat -= 12; jahr++; };
switch (monat) {
case 2: return (schaltJahr(jahr) ? 29 : 28);
case 4: case 6: case 9: case 11: return 30;
default: return 31;
}
}

long int minPerMonat ( int monat, int jahr) {


return MIN_PER_TAG*tagePerMonat(monat, jahr);
}

263
CDatum: Implementierung (1)

void CDatum::setAll(int j, int mo, int t, int s, int mi){


jahr = j; monat = mo; tag = t; stunde = s; minute =mi;
berechneMinuten();
berechneDatum(); //korrigiert, falls einer der Werte "illegal" sein sollte
}

void CDatum::berechneMinuten(){
long int lMinuten = 0;
for (int i=0; i < jahr; i++) lMinuten += minPerJahr(i);
for ( i=1; i < monat; i++) lMinuten += minPerMonat(i, jahr);
zeit = lMinuten + tag*MIN_PER_TAG + stunde*MIN_PER_STUNDE + minute;
}

void CDatum::zeige() {
cout << "Jahr " << jahr
<< " Monat " << monat
<< " Tag " << tag
<< " Stunde " << stunde
<< " Minute " << minute
<< endl;
}

264
CDatum: Implementierung (2)
void CDatum::berechneDatum(){
long int lZeit = 0;
int lJahr = 0;
int lMonat = 1;
if (zeit == 0)
jahr = monat = tag = stunde = minute = 0;
else {
jahr = 0;
monat = 1;
long int neu = minPerJahr(jahr);
while ( (lZeit + neu + MIN_PER_TAG) <= zeit ) { Ersetze die
jahr++;
lZeit += neu; Schleifen durch
neu = minPerJahr(jahr); Ausdrücke, die
}
neu = minPerMonat(monat, jahr);
sich mit
while ( (lZeit + neu + MIN_PER_TAG) <= zeit ) { konstanten
monat++; Aufwand
lZeit += neu;
neu = minPerMonat(monat, jahr); berechnen
} lassen!
tag = (zeit - lZeit)/MIN_PER_TAG;
stunde = (zeit%MIN_PER_TAG)/MIN_PER_STUNDE;
minute = zeit%MIN_PER_STUNDE;
}
}
265
CDatum: Hauptprogramm
int main() {
cout << "Anfang Test" << endl;
Anfang Test
CDatum meinDatum(2008,2,29,23,57); Jahr 2008 Monat 2 Tag 29 Stunde 23 Minute 57
CDatum xDatum(meinDatum); Jahr 2008 Monat 2 Tag 29 Stunde 23 Minute 58
meinDatum.zeige(); Jahr 2008 Monat 2 Tag 29 Stunde 23 Minute 59
Jahr 2008 Monat 3 Tag 1 Stunde 0 Minute 0
for (int i1 = 1; i1 < 8; i1++) { Jahr 2008 Monat 3 Tag 1 Stunde 0 Minute 1
meinDatum.addMinuten(); Jahr 2008 Monat 3 Tag 1 Stunde 0 Minute 2
meinDatum.zeige(); Jahr 2008 Monat 3 Tag 1 Stunde 0 Minute 3
} Jahr 2008 Monat 3 Tag 1 Stunde 0 Minute 4
cout << endl;
Jahr 2008 Monat 3 Tag 2 Stunde 0 Minute 4
meinDatum.addTage(); Jahr 2008 Monat 3 Tag 9 Stunde 0 Minute 4
meinDatum.zeige(); Jahr 2008 Monat 3 Tag 23 Stunde 0 Minute 4
meinDatum.addWoche();
meinDatum.zeige(); Jahr 2008 Monat 2 Tag 29 Stunde 23 Minute 57
meinDatum.addWoche(2); Jahr 2008 Monat 3 Tag 1 Stunde 23 Minute 57
meinDatum.zeige(); Jahr 2008 Monat 3 Tag 2 Stunde 23 Minute 57
cout << endl; Jahr 2008 Monat 3 Tag 3 Stunde 23 Minute 57
Jahr 2008 Monat 3 Tag 4 Stunde 23 Minute 57
xDatum.zeige(); Jahr 2008 Monat 3 Tag 5 Stunde 23 Minute 57
for (int i2 = 1; i2 < 10; i2++) { Jahr 2008 Monat 3 Tag 6 Stunde 23 Minute 57
xDatum.addTage(); Jahr 2008 Monat 3 Tag 7 Stunde 23 Minute 57
xDatum.zeige(); Jahr 2008 Monat 3 Tag 8 Stunde 23 Minute 57
} Jahr 2008 Monat 3 Tag 9 Stunde 23 Minute 57
}
266
static–Felder

 Die Klasse CDatum sei bereits definiert, dann werden durch


CDatum datum005(2008,2,29,23,57);
CDatum datum006(2006,11,11,11,11), datum007(2009,1,14,12,1);

drei verschiedene Objekte vom Typ CDatum definiert.


 Man spricht dann auch von "Instanzen der Klasse CDatum".
 In allen drei Instanzen haben die definierten Felder unterschiedliche
Werte und belegen unterschiedliche Speicherbereiche.
 Wenn ein Element einer Klasse den Modifikator static hat, ist sein Wert
in allen Instanzen der Klasse gleich. Man spricht dann auch von
Klassenfelder.
 Ein static–Feld muss außerhalb der Klassendefinition initialisiert werden.
 Ausnahmen sind konstante statische Felder.
 In dem folgenden Beispiel zählt das static-Feld zaehler die Anzahl der
bereits aktivierten Instanzen der Klasse CDatum.

267
CDatum: geändert

class CDatum {
static int zaehler;
Definition eines static Felds ...

public:
CDatum(){ zaehler++; ... }
In allen Konstruktoren
CDatum(int j, ... ){ zaehler++; ... }
wird der Zähler erhöht. CDatum(const CDatum& d)){ zaehler++;... }
...
};

Initialisierung int CDatum::zaehler = 1;


// Hier wird static nicht benutzt!

268
static–Methoden

 Methoden, die nur auf static–Felder zugreifen, können als static-


Methoden definiert werden.
 Diese Methoden können direkt über den Klassennamen aufgerufen
werden.
 Statische Methoden dürfen direkt nur auf statische Felder der
Klasse zugreifen und ggf. weitere statische Methoden aufrufen.
 Statische Methoden können allerdings Objekte der Klasse anlegen
und über diese die anderen Elemente der Klasse benutzen.
class CDatum {
static int zaehler;
...
public:
...
static void zeigeAnzahl(){
cout << "Anzahl der Instanzen :" << zaehler <<endl; }
...
};
// Aufruf
CDatum::zeigeAnzahl();
269
Destruktor

 Im Beispiel wird von jedem Konstruktor der Zähler erhöht, wenn ein
neues Datum erzeugt wird. Es fehlt das Gegenstück: der
Destruktor.
 Der Destruktor besitzt keine Parameter und darf nicht überladen werden.
 Wird der Destruktor nicht explizit angegeben, so wird automatisch ein
Destruktor hinzugefügt.
 Er wird aufgerufen, wenn die Instanz aufhört zu existieren – also bei
lokalen Objekten am Ende des Blockes in dem sie deklariert sind.
 Entsprechend wird bei einem global deklarierten und statischen Objekt der
Destruktor am Ende des Programmlaufs aufgerufen.
 Normalerweise sind Destruktoren zum Aufräumen und
Zurückgeben von Systemressourcen da:
 Freigabe von Speicherplatz,
Abbau der Unterobjekte, class CDatum {
Schließen von Dateien, … static int zaehler;
 Hier nutzen wir den Destruktor ...
public:
zum Dekrementieren des Zählers:
...
~CDatum(){ zaehler--; }
...
};
270
const-Objekte/Felder

 Wenn ein Feld einer Klasse den Modifikator const hat, muss es den
Wert in der Initialisierungsliste erhalten.
 Eine weitere Wertzuweisung ist nicht möglich.
 Eine Methode mit dem Modifikator const darf kein Feld der Klasse
verändern. Man spricht dann auch von Beobachtern.
 const steht hinter dem Funktionsnamen und gehört zur Signatur.
 Ein konstantes Objekt darf nur Beobachter aufrufen.
class CDatum {
Ein konstantes static int zaehler;
Feld const int nrInstanz;

public:
CDatum():nrInstanz(zaehler){ zaehler++;}
Eine konstante // usw. andere Konstruktoren
Methode int gibNr() const { return nrInstanz; }
};

Ein konstantes int CDatum::zaehler = 0;


const CDatum konDat(2002, 2, 2, 2, 2);
Datenobjekt
cout << konDat.gibNr();
271
Unterschied !

 Modifikatoren wie static und const beziehen sich ggf.


 auf die Elemente einer Klassendefinition und/oder
 auf die Definitionen von Objekten/Instanzen einer Klasse!

class CDatum {
static-, const- bzw. automatic-Felder einer static int zaehler;
Klassendefinition . const int nrInstanz;
long int zeit;
...
};

Definition von
Objekten/Instanzen dieser static CDatum statDat(2002, 2, 2, 2, 2);
Klassendefinition. const CDatum konDat(2003, 3, 3, 3, 3);
Die Objekte haben jeweils den CDatum automaticDat(2042, 4, 4, 4, 4);
Modifikator static, const, bzw.
keinen.

272
Dynamische Objekte

 Bisher haben wir im Wesentlichen nur die Erzeugung von


automatisch bzw. statischen Objekten betrachtet.
 Ihre Konstruktoren werden dann an der Deklarationsstelle
abgearbeitet, ihre Destruktoren bei Block- bzw. Programmende.
 Der Speicherplatz für automatische Objekte ist der Laufzeit-Stack
des aktuellen Programmteiles.
 Der Speicherplatz für static- Objekte ist ein spezieller Bereich des
aktuellen Programmteils.
 Was wir noch benötigen ist die dynamische Erzeugung von
Objekten.
 Dynamische Objekte werden im Heap-Speicher verwaltet.
 Benutzer ist verantwortlich für die Verwaltung der Objekte im Heap-
Speicher.

273
Dynamische Objekte in C++

 Dynamische Objekte werden mit dem Operator new erzeugt


und mit dem Operator delete vernichtet.
 Damit liegt die Lebensdauer eines dynamischen Objekts alleine
in der Verantwortung des Anwendungsprogramms.
 Achtung: Fehlerquelle
 Sie können zu einem beliebigen Zeitpunkt erzeugt und später
auch wieder vernichtet werden.
 Ein Konstruktor wird bei der Abarbeitung von new
aufgerufen und der Destruktor bei der Abarbeitung von
delete.

CDatum *meinDatum = new CDatum(2005,12,31,23,59);


CDatum *dat2(new CDatum(2006,12,31,1,0));
......

delete meinDatum;

delete dat2;
274
Zugriff auf Felder und Methodenaufruf

 Der Zugriff auf Felder und Methoden dynamischer Objekte


erfolgt nicht über die Punkt-Notation sondern mit dem
Symbol ->

CDatum *meinDatum = new CDatum(2005,12,31,23,59);


CDatum *dat2(new CDatum(2006,12,31,1,0));
......
cout << dat2->gibNr() << endl;
cout << meinDatum->gibNr() << endl;
dat2->zeige();
meinDatum->zeige();

275
Beispiel für Fehler
class CDatum {
long int zeit; int jahr, monat, tag, stunde, minute;
static int zaehler;
const int nrInstanz;
……
public:
CDatum(): zeit(0), nrInstanz(zaehler){berechneDatum(); zaehler++; }
……
void zeige() const;
int gibNr() { return nrInstanz; }
};
CDatum *meinDatum = new CDatum(2005,12,31,23,59);
CDatum *dat1 = new CDatum(2006,12,31,1,0);
CDatum *dat2(dat1);

delete meinDatum;
delete dat2;

CDatum *dat3(new CDatum(2042,12,31,1,0));


cout << meinDatum->gibNr() << endl;
cout << dat1->gibNr() << endl;
Wird gnadenlos akzeptiert, cout << dat2->gibNr() << endl;
dat1->zeige();
produziert aber Unsinn! dat2->zeige();
dat3->zeige();
276
this-Referenz

 Die Methoden einer Klasse können jederzeit über das


Schlüsselwort this auf die eigenen Felder zugreifen.
 this entspricht einem mit const deklarierten Pointer.
 Zugriff auf ein Feld meinFeld:
this->member = ...
 Häufig wird auch die Objekt/Referenz als Ergebnis nach außen
gegeben:
 return this;
 return *this;

277
Objekte als Parameter

 Objekte können wie üblich als Parameter an eine Funktion


übergeben werden.
 Nachteil: Objekte werden mit dem Copy-Konstruktor kopiert, was i. A. sehr
zeitaufwändig ist und überraschende Effekte verursacht.
 Objekte können als Resultat einer Funktion durch eine Kopie nach außen
geliefert werden.
 Wird ein Objektparameter als const deklariert, so gilt wie oben
schon gesagt, dass das Objekt nur Beobachter aufrufen darf.
 In Kombination mit dem Referenzoperator kann damit eine effiziente
Übergabe des Objekts erfolgen, ohne das dies zu einer Änderung des
Objekts führt.
 Ähnlich kann auch ein Resultat einer Funktion nach außen geliefert werden.
const CDatum &minDatum(const CDatum& d1,
const CDatum& d2) {...}
...
const CDatum &first = minDatum(birthday1, birthday2);
 Soll eine Funktion ein Parameterobjekt verändern, so sollte das
Objekt stets über einen expliziten Pointer übergeben werden.

278
Beispiel

 Funktionen
void f1(const CDatum &x) {
x.print();
}
void f2(CDatum x) {
x.print();
}
 Die Aufrufe der Funktionen f1 und f2 mit dem gleichen
Objekt als Parameter führen nicht zum gleichen Resultat!
 Warum?

279
Ein Beispiel: Listen

Listen sind ein gutes Beispiel für die Verwendung dynamischer Objekte

 Listen sind ein häufig verwendetes Organisationsprinzip zur Verwaltung


von Mengen homogener Objekte.
 Unterstützung von Einfügen, Suchen und Löschen von Elementen
 Implementierung einer einfach verketteten Liste:
 Anfangs-Pointer: Zeiger auf das erste Element
 End-Pointer: Zeiger auf das letzte Element.
 Jedes Listenelement besteht aus beliebigem Inhalt und einem Verweis auf
das nächste Listenelement.

Nullpointer

Anfang Inhalt Inhalt Inhalt Inhalt

Ende
 Der Inhalt kann einen beliebigen Datentyp haben.

280
Listen von Daten (1)
 Mit den bisherigen Definition können wir sehr einfach eine Liste definieren, bei
der der Inhalt jeder Zelle gerade ein Datum vom Typ CDatum ist.
 Wir definieren zunächst einen Pointer auf ein Listenelement:

typedef class ListenElement *LEP;

 Nunmehr können wir eine Klasse für die Listenelemente selbst definieren:

class ListenElement {
public:
CDatum datum;
LEP nachfolger;
ListenElement (const CDatum& d, LEP n)
: datum(d), nachfolger(n)
{}
};

281
Listen von Daten (2)
 Für die Bearbeitung einer kompletten Liste mit Anfangs- und Endpointer
können wir folgende Klasse definieren:

class ListenKlasse {
LEP anfang;
LEP ende;
public:
ListenKlasse () : anfang(NULL), ende(NULL) {}
~ListenKlasse();
void einfuegenAnfang(const CDatum& d);
void einfuegenEnde(const CDatum& d);
void ausgabe();
};

 Der Konstruktor erzeugt eine leere Liste.


 Die beiden Methoden fügen ein Listenelement jeweils am Anfang bzw. am Ende
der Liste ein.
 Der Destruktor soll die gesamte Liste abarbeiten und die einzelnen Elemente
wieder löschen.
282
Listen von Daten (3)

 Implementierung des Destruktors:

ListenKlasse::~ListenKlasse() {
LEP zp(anfang);
while ( anfang != NULL) {
anfang = anfang -> nachfolger;
delete zp;
zp = anfang;
}
}

 zp ist jeweils der Anfang der aktuellen Liste.


 Nachdem der Anfang auf das nächste Element fortgeschrieben wurde,
kann der alte Anfang gelöscht werden.

283
Listen von Daten (4)
 Implementierung des Einfügens am Anfang der Liste:

void ListenKlasse::einfuegenAnfang(const CDatum& d) {


LEP zp(new ListenElement(d, NULL));
if (anfang == NULL) anfang = ende = zp;
else {
zp->nachfolger = anfang;
anfang = zp;
}
}

 Es wird ein neues Listenelement zp mit dem neuen Datum erzeugt.


 Falls die Liste noch leer ist, werden Anfang und Ende auf zp gesetzt.
 Andernfalls wird der Nachfolger von zp der bisherige Anfang und der neue
Anfang zp.
zp Neu D Inhalt

neu bisher

anfang
284
Listen von Daten (5)
 Implementierung des Einfügens am Ende der Liste:

void ListenKlasse::einfuegenEnde(const CDatum& d) {


LEP zp(new ListenElement(d, NULL));
if (anfang == NULL) anfang = ende = zp;
else {
ende->nachfolger = zp;
ende = zp;
}
}

 Es wird ein neues Listenelement zp mit dem neuen Datum erzeugt.


 Falls die Liste noch leer ist, werden Anfang und Ende auf zp gesetzt.
 Andernfalls wird zp der Nachfolger des bisherigen Endes und das
neue Ende.
Inhalt Neu D

bisher neu

ende zp
285
Listen von Daten (6)

 Implementierung der Ausgabe einer Liste:

void ListenKlasse::ausgabe() {
LEP zp(anfang);
while ( zp != NULL) {
zp->datum.zeige();
zp = zp->nachfolger;
}
}

 Dabei setzen wir voraus, dass in CDatum wie bereits beschrieben eine
Methode zeige implementiert ist.

286
Aufbau einer Liste (1)

 Man kann mit den bisherigen Klassendefinitionen bzw. Methoden eine


Liste von Daten aufbauen. Z.B.:
anfang
ListenKlasse neueListe;
ende

 neueListe ist eine (automatische) Variable. Der Konstruktor der Klasse


erzeugt eine leere Liste.
 Jetzt können wir ein erstes Element einfügen (egal ob am Anfang oder
Ende):

neueListe.einfuegenAnfang(CDatum(2005,12,31,23,59));

anfang 31.12.2005 23:59

ende
287
Aufbau einer Liste (2)
 Einfügen eines weiteren Elementes am Ende der Liste:

neueListe.einfuegenEnde(CDatum(2006,12,31,23,59));

anfang 31.12.2005 23:59 31.12.2006 23:59

ende

 Einfügen eines weiteren Elementes am Anfang der Liste:

neueListe.einfuegenAnfang(CDatum(2004,12,31,23,59));

anfang 31.12.2004 23:59 31.12.2005 23:59 31.12.2006 23:59

 usw. ende
 Anschließend können wir die Liste ausgeben: neueListe.ausgabe();

288
Aufbau einer Liste (3)
#include <iostream>
Programmstruktur:
using namespace std;
#include "CDatum3Inc.cpp"
typedef class ListenElement * LEP;
class ListenElement { … }
class ListenKlasse { … }

int main(){
cout << "Anfang Test" << endl;
ListenKlasse neueListe;
neueListe.einfuegenAnfang(CDatum(2005,12,31,23,59));
neueListe.einfuegenEnde(CDatum(2006,12,31,23,59));
neueListe.einfuegenAnfang(CDatum(2004,12,31,23,59));
neueListe.ausgabe();
}

ListenKlasse::~ListenKlasse() { … }
void ListenKlasse::einfuegenAnfang(const CDatum& d) { … }
void ListenKlasse::einfuegenEnde(const CDatum& d) { … }
void ListenKlasse::ausgabe() { … }
289
Alternative zum Einfügen am Ende:

typedef class ListenKlasse * LKP;


LKP ListenKlasse::einfuegenEnde(const CDatum& d) {
LEP zp(new ListenElement(d, NULL));
if (anfang == NULL) anfang = ende = zp;
else {
ende->nachfolger = zp;
ende = zp;
}
return this;
}

int main(){
cout << "Anfang Test" << endl;
LKP l = new ListenKlasse();
l->einfuegenEnde(CDatum(2004,12,31,23,59))
->einfuegenEnde(CDatum(2005,12,31,23,59))
->einfuegenEnde(CDatum(2006,12,31,23,59));
l->ausgabe();
}
290
Wiederverwendung von Klassen
Komposition, Vererbung und Polymorphie
Inhalt

 Motivation
 Syntax für die Kompostion
 Syntax für die Vererbung
 Vererbung von public / private / protected
 Hierarchien
 Polymorphie
 Konstruktoren und Initialisierung
 Konstruktion abgeleiteter Klassen
 Überdeckung von Bezeichnern
 virtuelle Elementfunktionen
 Regeln
 Mehrfachvererbung

292
Motivation

 Klassenentwicklung auf Basis der Wiederverwendung des bisherigen Codes


 Bisheriger Code darf dabei nicht verändert werden.
 Bisheriger Code soll nicht über Copy&Paste wieder verwendet werden.
 Wichtiges Ziel bei der Entwicklung von Programmen:
Vermeidung von Redundanz
 Zwei Techniken
 Komposition
 Vererbung
 Leider wird Wiederverwendung oft mit Vererbung gleichgesetzt.
Deshalb hier schon eine kurze Anmerkung zur Vererbung:
 Vererbung bezieht sich im wesentlichen auf die Eigenschaft der Klasse als
Datentyp.
a) Neue Klassen werden erstellt als Typ einer Basisklasse.
b) Abgeleitete Klassen sind vom Typ her Spezialfälle der Basisklasse
Auf Vererbung in Sinne b) wird erst später eingegangen.

293
Beispiel für die Komposition

 Komposition bedeutet, dass Attribute in Klassen Objekte aus


einer anderen Klasse sein dürfen.
 Man spricht dann auch von hat-eine-Beziehung.
(has-a relation).
 Ein Objekt b der Klasse B hat ein Objekt a der Klasse A als Element.
 Beispiel:

class A { class B {
int x; A a; // B hat ein A
public: public:
void setX() {x = 42;} void setAX(){a.setX();}
int getX(){return x;} int getAX(){ return a.getX(); }
}; };

B b;
b.setAX();
cout << b.getAX();

294
Transitivität

 Komposition darf ohne Einschränkungen immer wieder


angewendet werden.
class C {
B b;

}

 Die Klasse B hat ein Feld der Klasse A.


 Die Klasse C hat ein Feld der Klasse B.
 Indirekt hat somit C Zugriff auf ein Objekt der Klasse A.

295
Konstruktoren bei Komposition

 Problemstellung
 Wie werden bei Komposition die zugehörigen Objekte erzeugt und
initialisiert?
 Bei Erzeugung eines Objekts wird zunächst der eigene
Konstruktor aufgerufen.
 Danach werden die Objekte erzeugt, die als Felder in der
Klasse verwendet werden.
 Unterscheidung: impliziter und expliziter Konstruktion:
 Impliziter Aufruf des parameterlosen Konstruktors
 Expliziter Aufruf eines Konstruktors muss mittels der
Initialisierungsliste erfolgen.
class C{
B b;

C() : b(…) { …} // expliziter Aufruf der Konstrukoren

}
296
Beispiel

class A { class C {
int x; B b; // C hat ein B
public: public:
A(int y) : x(y){}; C() : b(4711){};
void setX() {x = 42;} B getB(){ return b; };
int getX(){return x;} };
};

class B { int main(){


A a; // B hat ein A C c;
public: cout << c.getB().getAX();
B(int y) : a(y) {}; }
void setAX(){a.setX();}
int getAX(){ return a.getX(); }
4711
};

297
Vererbung (1)

 Eine Klasse definiert die Eigenschaften und das Verhalten ihrer


Objekte.
 In objektorientierten BK Basisklasse
Programmiersprachen
kann man eine Klasse
aus einer anderen ableiten.
 Man spricht dann von Vererbung AK Abgeleitete Klasse

 Die abgeleitete Klasse erbt alle Felder, Methoden und


Operatoren der Basisklasse, hat aber nicht notwendig das
Zugriffsrecht auf diese.
 Die abgeleitete Klasse erweitert die Basisklasse um neue
Datenfelder und Methoden.
 Statt Basisklasse sprechen wir auch von der Oberklasse und
statt abgeleitete Klasse benutzen wir Unterklasse.

298
Vererbung (2)

 Sei eine Klasse A gegeben (Oberklasse). Umfasst eine Klasse B


(Unterklasse) alle Eigenschaften (Felder und Methoden) der Klasse
A, so ist Klasse B eine Spezialisierung der Klasse A.
 Vererbung ist zunächst einmal eine syntaktische Unterstützung zur
Erstellung spezialisierter Klassen.
 kein Duplizieren von Deklarationen
 Vermeidung von Fehlern
 Die Vererbung ist transitiv.
 Einfachvererbung liegt vor wenn
 eine Unterklasse genau eine Oberklasse besitzt.
 Mehrfachvererbung liegt vor wenn
 eine Unterklassen mehrere Oberklassen besitzt.
 Problem: Namenskonflikte, wenn Methoden mit gleicher Signatur sich in
verschiedenen Oberklassen befinden.

299
Vererbung (3)

 Es gibt drei verschiedene Varianten der Vererbung:


class neueKlasse : public basisKlasse { … }

 Alle Elemente der Basisklasse werden in der abgeleiteten Klasse


übernommen - insbesondere werden auch die Modifikatoren der Elemente
unverändert gelassen.
 Dies ist der wichtigste Vererbungstyp (einige meinen sogar der einzig
sinnvolle Typ). Dieser wird im weiteren Verlauf des Kapitels intensiv
diskutiert.
class neueKlasse : protected basisKlasse { … }

 Alle nicht-privaten Elemente werden als protected markiert.

class neueKlasse : private basisKlasse { … }


 Alle Elemente der Basisklasse werden als private markiert.
 Alle nicht-privaten Elemente werden als private markiert.
 Dies ist auch der Standardfall, falls kein Modifikator vor der
Basisklasse steht.

300
Vererbung (4)

Man muss explizit sagen, dass man und wie man erben will:
class AK : public BK { }; Viel Funktionalität aufrufbar
class AK : protected BK { };

class AK : BK { }; Wenig Funktionalität aufrufbar

Ableitungs- Zugriff auf Elemente in abgeleiteten Klassen:


Methode:
private-Elemente protected -Elemente public-Elemente

public: kein Zugriff protected public

protected: kein Zugriff protected protected

private: kein Zugriff private private

301
Vererbung (6)

 Methoden
 Die abgeleitete Klasse erbt auch alle Methoden und Operatoren der
Basisklasse.
 Benutzen darf sie diese nur, wenn sie Zugriffsrecht hat.
 Sie darf diese Methoden aber ggf. neu definieren.
 Diese Eigenschaft, dass Namen von Methoden in abgeleiteten Klassen
unterschiedlich definiert und verwendet werden können, bezeichnet man
in der Literatur auch als Ad-hoc-Polymorphie.

303
Ein Beispiel für Vererbung:
class A {
int x;
public:
A(int y) : x(y){};
void setX() {x = 42;}
int getX(){return x;}
};

class B: private A {
int neu; // neues Feld
public:
B() : neu(1), A(12) {};
void setNeu(int i) {neu = i;} // neue Methoden
int getNeu(){return neu;}
int getAX() {return getX();} // Liefert x von A.
};

int main(){
B b;
cout << b.getAX(); 12
}
304
Vererbungshierarchien

 Den Vorgang der Vererbung kann man wiederholt anwenden.


(Transitivität)
 Man kann so ganze Vererbungshierarchien definieren.
 Eine abgeleitete Klasse erbt die in der Basisklasse
definierten Elemente und die Elemente, die diese selbst
wieder von ihren Vorfahren geerbt hat usw....

305
Konstruktoren bei Vererbung (1)

 Problemstellung
 Wie werden bei Vererbung die Objekte der abgeleiteten Klassen
erzeugt und initialisiert?
 Bei Erzeugung eines Objekts wird zuerst ein Konstruktor der
Basisklasse aufgerufen.
 Danach erst wird ein Konstruktor der abgeleiteten Klasse
ausgeführt.
 Unterscheidung: impliziter und expliziter Konstruktion:
 Impliziter Aufruf des parameterlosen Konstruktors
oder
 Expliziter Aufruf eines Konstruktors muss in der Initialisierungsliste
der abgeleiteten Klasse erfolgen.
class D: public C {

D() : C(…) {…} // expliziter Aufruf der Konstrukoren

}
306
Konstruktoren bei Vererbung: Fall 1

 Welcher Konstruktor der Basisklasse soll gerufen werden?


 Ohne explizite Angabe wird der Default-Konstruktor gerufen. Dieser
muss dann aber auch aufrufbar sein.

class Basis {
int x;
public:
Basis() : x(0) {}
Basis(int pb) : x(pb) {}
Basis(const Basis &b): x(b.x) {}
int getX(){return x;}
class Abgeleitet : public Basis {
};
public:
Abgeleitet(int y) : Basis(y) {}
Abgeleitet(const Abgeleitet &a) {}
int main() {
};
Abgeleitet a1(4711);
Abgeleitet a2(a1); // Kopierkonstruktor
cout << "a1.x: " << a1.getX() << " a2.x: " << a2. getX();
cout << endl;
} a1.x: 4711 a2.x: 0
307
Konstruktoren bei Vererbung: Fall 2

 Welcher Konstruktor der Basisklasse soll gerufen werden?


 Ohne explizite Angabe wird der Default-Konstruktor gerufen. Dieser
muss dann aber auch aufrufbar sein.

class Basis {
int x;
public:
Basis() : x(0) {}
Basis(int pb) : x(pb) {}
Basis(const Basis &b): x(b.x) {}
int getX(){return x;}
}; class Abgeleitet : public Basis {
public:
Abgeleitet(int y) : Basis(y) {}
Abgeleitet(const Abgeleitet &a):Basis(a){}
int main() { };
Abgeleitet a1(4711);
Abgeleitet a2(a1); // Kopierkonstruktor
cout << "a1.x: " << a1.getX() << " a2.x: " << a2. getX();
cout << endl;
} a1.x: 4711 a2.x:4711
308
Zugriffsrechte bei abgeleiteten Klassen (1)

 Die abgeleitete Klasse kann unabhängig davon wie geerbt


wurde auf alle mit public und protected markierten Felder der
Basisklasse zugreifen.
 Wird in der abgeleiteten Klassen ein Objekt der Basisklasse erzeugt,
gilt der übliche Zugriffsschutz.

class Basis {
public:
int a, b, c;
protected:
int z;
};
class Child : protected Basis {
public:
int h(Basis other) {
int y = z; // ok
int no = other.z; // Dies ist nicht erlaubt!
}
};
309
Zugriffsrechte bei abgeleiteten Klassen (2)

Verfeinerung des Zugriffsschutzes


 Möchte man nun oben das Feld a der Klasse Child als public
markieren, dann kann dies durch
public: Basis::a;
erfolgen.

 Eine Aufweichung des Zugriffsschutzes ist nur bedingt möglich:


 Ein private-Element der Basisklasse kann nicht als protected/public
in der abgeleiteten Klasse deklariert werden.
 Ein geerbtes protected-Element kann aber als private/public
deklariert werden, ebenso kann ein public-Element private/protected
deklariert werden.
 Argumentation: Da man in der abgeleiteten Klasse eine neue public
Methode schreiben kann, die als Ausgabe das als protected geschützte
Attribut nach außen gibt, kann man den Zugriff auch explizit
erlauben.

310
Überdeckung von Bezeichnern (1)

 Problemstellung:
 Was passiert in einer abgeleiteten Klasse, wenn eine Methode den
gleichen Namen hat, wie in der Basisklasse?
 Fallunterscheidung
 Gleiche Signatur mit gleichem Rückgabetyp:
Dies tritt typischerweise bei public-Vererbung auf und bei so
genannten virtuellen Methoden. Eine genaue Diskussion dieses
wichtigen Falls erfolgt später.
 Unterschiede in der Signatur oder/und dem Rückgabetyp:
Damit steht die Implementierung aus der Basisklasse nicht mehr
direkt zur Verfügung.
Beispiel:
 getX-Methode der Klasse A steht nicht mehr direkt (!) für Objekte
der Klasse E zur Verfügung. class E: public A {
double x;
public:
double getX() { return x; }
};
311
Überdeckung von Bezeichnern (2)

 Zugriff auf verdeckte Elemente erfolgt über den Scope-


Operator
 Beispiel:
E e;
e.getX(); // Aufruf von getX aus der Klasse E
e.A::getX(); // Aufruf von getX aus der Klasse A.

312
Zuweisung und Typumwandlung (0)

 Für die folgenden Folien werden folgende Klassen benutzt:


class MyBase {
public:
int a;
MyBase():a(1){}
void f() { cout << "f-Basis. a = " << a << endl; }
};

class MyChild : public MyBase {


public:
MyChild(){ a=2; }
void f() { cout << "f-Child. a = " << a << endl; }
void g() { cout << "g-Child" << endl; }
};

313
Zuweisung und Typumwandlung (1)

 Bei einer public-Vererbung ist es möglich Objekte einer


abgeleiteten Klasse ohne expliziten Cast einer Variable der
Basisklasse zuzuweisen.
 Über die Variable werden die Felder und Methoden der Basisklasse
angesprochen!
 Umgekehrt ist es nicht möglich, Objekte einer Basisklasse einer Variable
einer abgeleiteten Klasse zuzuweisen – auch nicht mit explizitem Cast .

int main(){
MyBase m;
f-Basis. a = 1
MyChild c;
m.f(); c.f(); f-Child. a = 2
m=c; // Ist erlaubt: s.o.
m.f(); // Ist erlaubt. f-Basis. a = 2
c.f(); // Ist erlaubt.
m.g(); // Nicht erlaubt: g ∉ MyBase f-Child. a = 2
c=m; // Nicht erlaubt.
}
314
Zuweisung und Typumwandlung (2)

 Erklärung
int main(){
MyBase m;
MyChild c;
m=c; // Objekt der Klasse MyChild wird mittels des
// Zuweisungsoperators (= Default-CopyKonstruktor!)
// in eine Variable der Klasse MyBase kopiert!
}

 In C++ ist m eine normale Variable und keine Referenzvariable


(=Pointervariable) wie etwa in Java.
 Bei der Zuweisung werden daher die Felder des Objektes der
abgeleiteten Klasse an die entsprechenden Felder der Basisklasse
zugewiesen.
 Die Methoden von m sind die der Basisklasse und nicht die der
abgeleiteten. Die Methode g() ist also nicht benutzbar.

315
Zuweisung und Typumwandlung (3)

 Wenn umgekehrt die abgeleitete Klasse einen passenden


Konstruktor hat, der einen Parameter vom Typ der Basisklasse hat,
dann kann man auch die umgekehrte Zuweisung ausführen:
class MyChild : public MyBase {
public:
MyChild(){a=2;}
MyChild(const MyBase& m){ a = m.a;}
void f() {cout << "f-Child. a = " << a << endl;}
void g() {cout << "g-Child" << endl;}
};

int main(){
MyBase m;
MyChild c;
c.f(); f-Child. a = 2
c=m; // jetzt erlaubt!
c.f(); f-Child. a = 1
}

316
Zuweisung und Typumwandlung (4)

 Bei Zeigervariablen ist die Zuweisungsproblematik ähnlich.


 Wenn statt der normalen Variablen eine Pointervariable verwendet
wird, zeigt die Variable ggf. auf ein Objekt der abgeleiteten Klasse:
MyBase m;
MyChild c;
MyBase *pm = &m; // Kein Aufruf des Copy-Konstruktors !!
// pm verweist auf ein Objekt der Klasse MyBase.
MyChild *pc = &c; // Kein Aufruf des Copy-Konstruktors !!
// pc verweist auf ein Objekt der Klasse MyChild.

 Eine Zuweisung eines Pointers auf ein Objekt einer abgeleiteten


Klasse an ein Objekt der Basisklasse ist möglich:

pm = pc; // Ist möglich !!
// aber Zugriff nur auf Elemente der Basisklasse
pm->g(); // Somit ist dies nicht möglich!

 Kann man auch nicht mit zusätzlichem Konstruktor "reparieren".


317
Zuweisung und Typumwandlung (5)

 Umgekehrt: Möchte man einen Pointer auf MyBase an einen


Pointer auf MyChild zuweisen, muss man explizit casten:

pc->g(); g-Child
pc = (MyChild *) pm;
pc->g(); g-Child

 C++ prüft nicht, ob pm tatsächlich auf ein zulässiges Objekt zeigt.


 Daher sollte, der zweite Funktionsaufruf eigentlich nicht
funktionieren!
 Funktioniert aber im Beispiel! Warum?
 C++ überprüft nicht, ob pm tatsächlich auf ein zulässiges Objekt
zeigt, daher ist es besser, Cast-Operationen (insbesondere
Downcasts) über sogenannte dynamic_cast umzusetzen.

318
Zuweisung und Typumwandlung (6)

 Diese Form des Casts wird wie folgt verwendet:


dynamic_cast<type>(expression)

 Dadurch wird zur Laufzeit geprüft, ob der Cast auch wirklich


erlaubt ist. Hierzu ist es insbesondere notwendig, dass das
Laufzeitsystem Typinformationen verwaltet.
 Dies muss explizit dem Compiler (z. B. GNU) durch die Option -frtti
mitgeteilt werden. Bei Microsoft /GR

Beispiel: (wird von Microsoft aber so nicht akzeptiert)


MyChild *x = dynamic_cast <MyChild *> (pm);

 Hier gibt es schon einmal einen ersten Vorgeschmack auf


generische Datentypen in C++!

319
Polymorphie (1)

 Polymorph = vielgestaltig
 In der Literatur wird unterschieden zwischen
 Polymorphie bei Methoden (Ad-hoc-Polymorphie)
 Polymorphie bei Klassen
 Ad-hoc-Polymorphie bei Methoden
 auch bekannt als Überschreiben von Methoden
 Die gleiche Methode kann in verschiedenen Klassen eines
Vererbungspfads verschiedenartig implementiert sein.

320
Überladen und Überschreiben

 Überladen von Methoden


 Verschiedene Methoden in einer Klasse besitzen den gleichen Namen.
 Die Signatur der Methoden muss eindeutig sein.
 Besitzen von einer Oberklasse geerbte Methoden identische Namen,
wie in der Unterklasse deklarierte Methoden, so werden sie verdeckt,
können aber durch eine using-Deklaration zugreifbar gemacht werden
 Überschreiben von Methoden
 Neuimplementierung einer Methode in einer Unterklasse. Die neue
Methode besitzt insbesondere die gleiche Signatur wie eine Methode
der Oberklasse.
class B : public A{
class A{ public:
public: void f(double d){ … }
void f(){ … } // lädt die Funktionen mit Namen f aus
void f(int i){ … } // dem Scope von A in den von B
void g(){ … } // => f ist in B dreifach überladen
}; using A::f;
void g() { … } // überschreibt A::g
};
321
Polymorphie (2)

 Typenkonzept
 strenge Typisierung: Festlegung des Typs einer Variablen zur
Übersetzungszeit
 schwache Typisierung: Variablen besitzen keinen Typ (z. B. LISP)
 Polymorphie
 Sei v eine Variable vom Typ T. Dann ist es möglich, Objekte von der
Klasse T oder von deren Unterklasse U an v zu übergeben.
 Bei einem Aufruf einer Methode mit einem formalen Parameter vom
Typ T, darf der aktuelle Parameter vom Typ T oder vom Typ U sein.
 Polymorphe Vererbung
 Polymorphie ist i. A. nur dann sinnvoll, wenn zwischen den Objekten
der Ober- und Unterklasse eine IS-A-Beziehung besteht.
 In C++ ist es z.B. möglich, nicht-polymorphe Vererbung zwischen
Klassen zu deklarieren.
 In Java kann Polymorphie nicht unterbunden werden.

322
Beispiel

 Wir definieren eine Klasse für grafische Objekte.


 Es fehlen die Implementierung des Destruktors und der Methoden
zeichne und loesche.
class GrafikObjekt{
private:
int xpos;
int ypos;
public:
GrafikObjekt(): xpos(0), ypos(0) {};
GrafikObjekt(int x, int y) : xpos(x), ypos(y) {}
~GrafikObjekt();
void zeichne();
void loesche();
void setPos(int x, int y) {xpos = x; ypos = y;}
void verschiebe(int x, int y){
loesche();
xpos += x; ypos += y;
zeichne();
}
};
323
Erweiterung

 Ausgehend von der Klasse GrafikObjekt wollen wir Klassen für


Rechtecke und Quadrate definieren.
GrafikObjekt

GRechteck GKreis

GQuadrat

 Dies kann eine sinnvolle Erweiterung sein, da ein Quadrat auch


ein Rechteck und ein Rechteck auch ein GrafikObjekt ist.
 Diese ist-ein-Beziehung (isA-Relation) sollte beim Bilden von
Erweiterungsklassen immer erfüllt sein.
 Wir können auch noch eine Klasse für Kreise definieren. Ein
Kreis ist ebenfalls ein GrafikObjekt steht aber in keiner ist-ein-
Beziehung zu Rechteck oder Quadrat.

324
Die Klasse GRechteck

 Diese Klasse hat zusätzliche Felder: breite und hoehe.


 Die Methoden zeichne und loesche müssen wahrscheinlich
angepasst werden.
 Diese Methoden werden überschrieben.

class GRechteck : public GrafikObjekt{


private:
int breite, hoehe;
public:
GRechteck () : GrafikObjekt(0,0), breite(0), hoehe(0){}
GRechteck (int x, int y, int b, int h) :
GrafikObjekt(x,y), breite(b), hoehe(h) {}
void zeichne();
void loesche();
};

325
Die Klasse GQuadrat

 Diese Klasse hat keine zusätzlichen Felder – im Gegenteil es könnte


eines der Felder der übergeordneten Klasse eingespart werden.
 Der einzige Vorteil der neuen Klasse sind (abgesehen davon, dass wir
ein schönes Beispiel haben) sind die vereinfachten Konstruktoren für
Quadrate.
 Vermutlich können wir auch die Methoden zeichne und loesche
unverändert übernehmen.
 Um des lieben Beispieles willen, unterstellen wir jedoch das sie angepasst
werden müssen.

class GQuadrat : public GRechteck {


public:
GQuadrat () : GRechteck () {}
GQuadrat (int x, int y, int b) : GRechteck (x,y,b,b){}
void zeichne();
void loesche();
};
326
Implementierung von zeichne

 Wir wollen also eine neue Implementierung von zeichne für


Quadrate definieren.
 Es könnte nun aber sein das wir dabei die vorhandene
Implementierung der übergeordneten Klasse nutzen wollen. Dies
kann man mit dem Scope-Operator ::

void GQuadrat::zeichne(){
GRechteck::zeichne();Aufruf der Methode aus der Klasse Rechteck
...
}

327
Konstruktion abgeleiteter Klassen

 Die Konstruktoren von GQuadrat haben die entsprechenden


Konstruktoren von GRechteck aufgerufen und diese wiederum die
von GrafikObjekt.
GQuadrat () : GRechteck (0) {}
GQuadrat (int x, int y, int b) : GRechteck (x,y,b,b){}

 Man könnte auf die Idee kommen auch hierfür den gerade
definierten Scope-Operator :: einzusetzen. Das geht aber nicht!
 Um den Konstruktor der übergeordneten Klasse anwenden zu
können, müssen wir eine Konstruktor-Initialisierungsliste benutzen!
 Der Aufruf des übergeordneten Konstruktors mit dem Scope-
Operator :: führt zwar dazu das seine Anweisungen ausgeführt
werden – aber:
 Die vom Konstruktor zusätzlich durchgeführten
Speicherplatzanforderungen und Initialisierungen werden gar nicht
oder doppelt gemacht (je nach Implementierung).

328
Die Klasse GKreis

 Der Vollständigkeit halber hier noch eine Definition der Klasse


GKreis – obwohl nichts neues hinzukommt.
 Diese Klasse hat ein zusätzliches Feld: radius.
 Die Methoden zeichne und loesche müssen wahrscheinlich
angepasst werden.
 Wir nehmen an, dass diese Methoden überschrieben werden.

class GKreis : public GrafikObjekt{


private:
int radius;
public:
GKreis () : GrafikObjekt(0,0), radius(0) {};
GKreis (int x, int y, int r) :
GrafikObjekt(x,y), radius(r) {};
void zeichne();
void loesche();
};
329
Verschieben von GrafikObjekten

 In der Definition der Klasse GrafikObjekt hatten wir eine Methode zum
Verschieben von GrafikObjekten definiert:
void verschiebe(int x, int y){
loesche();
xpos += x; ypos += y;
zeichne();
}

 Da diese nur die Position verändern muss, braucht sie nicht auf die
Felder breite, hoehe, radius der abgeleiteten Klassen zuzugreifen.
 Man könnte sie also für alle bisher definierten Klassenerweiterungen
einfach übernehmen.
 Allerdings funktioniert das noch nicht ganz: Welches Exemplar von
loesche und zeichne wird angewendet?
 Bei dem sogenannten statischen Binden von Namen wird die Methode
vom Compiler ausgewählt, das ist die Methode aus der Klasse
GrafikObjekt.
 Das ist aber nicht die Zuordnung, die wir wollen!

330
Dynamisches Binden

 Die Lösung des Problems besteht darin, zeichne und loesche als
virtuell (Schlüsselwort virtual) zu deklarieren.
 Dann wird eine dynamische Zuordnung gemacht und im Falle
unseres Beispiels jeweils die richtige Methode zum Zeichnen bzw.
Löschen aufgerufen.
 Das hat zur Folge, dass eine Zuordnungstabelle erstellt wird, die bei
Aufrufen immer die Funktion aus der richtigen Klasse zuordnet.
Diese Tabelle wird automatisch angelegt und später auch wieder
automatisch gelöscht und aus dem Speicher entfernt.
 Da für die Zuordnung virtueller Funktionen zu ihren Klassen eine
umfangreiche Zuordnungstabelle aufgebaut werden muss, wird
im allgemeinen geraten, mit der Deklaration virtueller Funktionen
sparsam umzugehen.

331
Statisches und dynamisches Binden

 Werden Methoden überschrieben, so ergibt sich das Problem,


welche Implementierung tatsächlich beim Aufruf einer Methode
benutzt wird.

Statisches Binden von Methoden:


 Festlegen der Zuordnung von ausführbaren Code und
Methodenaufruf bei der Übersetzung auf Basis der Klasse der
Variablen.

Dynamische Binden von Methoden:


 Festlegung der Zuordnung von ausführbaren Code und
Methodenaufruf zur Laufzeit auf Basis der Klasse des Objekts.
 Man spricht dann auch von virtuellen Methoden.

332
GrafikObjekt: endgültig

 Die Methode verschiebe braucht nicht als virtuell definiert zu


werden! zeichne und loesche müssen aber virtuell sein!
class GrafikObjekt{
private:
int xpos;
int ypos;
public:
GrafikObjekt(): xpos(0), ypos(0) {}
GrafikObjekt(int x, int y) : xpos(x), ypos(y) {}
~grafikobjekt();
virtual void zeichne();
virtual void loesche();
void setPos(int x, int y) {xpos = x; ypos = y;}
void verschiebe(int x, int y){
loesche();
xpos += x; ypos += y;
zeichne();
}
};
333
Virtuelle Methoden (1)

“Virtuelle Methoden” lösen ein technisches Problem des Compilers:

 Bei einem normalen Übersetzungsvorgang werden


Prozeduraufrufe statisch den nächstliegenden
vergangenen Prozedurdefinitionen zugeordnet.

 Dies können wir aber in diesem Zusammenhang nicht


gebrauchen!

 Wir müssen im Kontext von GRechteck die Methoden


loesche und zeichne von Punkt und im Kontext von
GKreis bzw. GQuadrat die dort jeweils definierten
Methoden benutzen.
 Dies wird durch die Verwendung virtueller Methoden
ermöglicht.

334
Virtuelle Methoden (2)

 Virtuelle Methoden werden dynamisch zur Laufzeit aus


einer Tabelle ausgewählt.
 Diese Tabelle muss von einem Konstruktor angelegt
werden.

 Dies ist eine Initialisierungsmethode, die vor allen


anderen Methoden aufgerufen wird und als Nebeneffekt
die Tabelle aufbaut.
 Der Destruktor gibt den Speicherplatz für die Tabelle
wieder frei.

335
Eigenschaften virtueller Methoden

1. Methoden können überschrieben werden, auch wenn sie


nicht virtuell sind.

2. Eine nicht-virtuelle Methode kann eine virtuelle Methode


aufrufen.

3. Virtuelle Methoden können nicht-virtuelle Methoden in


Vorgängerklassen überschreiben.

Wenn in einer Klasse eine Methode virtuell ist, sind die


4.
überschreibenden Methoden in den abgeleiteten Klassen
ebenfalls virtuell. („Einmal virtuell - immer virtuell“).

Im Zweifelsfall sollte eine Methode sicherheitshalber als


5.
virtuell gekennzeichnet werden.

336
Wirksamkeit von dynamischen Binden

 Gewöhnungsbedürftig ist die Tatsache in C++, dass nur


dann dynamisch gebunden wird, wenn der Zugriff über eine
Pointervariable oder Referenzvariable erfolgt.
 Beispiel
Quadrat q = new Quadrat();
GrafikObjekt go(q);
GrafikObjekt & rg = q; // Referenzvariable rg
rg.zeichne(); // Es wird zeichne aus Quadrat gerufen.
go.zeichne(); // Aufruf von zeichne aus GrafikObjekt

337
Noch ein Beispiel für virtuelle Methoden

 Siehe auch Beispiele auf Folien „Zuweisung und Typumwandlung“


class MyBase {
public:
virtual void f() {cout << "Basis" << endl;};};
 Dies ist dann nur sinnvoll, wenn in einer abgeleiteten Klasse die
Methode tatsächlich überschrieben wird (gleiche Signatur, gleicher
Rückgabetyp).
class MyChild : public MyBase {
public:
void f() {cout << "Child" << endl;}
// f ist ebenfalls virtuell: einmal virtuell immer virtuell
};

 Der virtuelle Effekt zeigt sich, wenn Zeigervariablen oder


Referenzvariablen benutzt werden:
MyBase * pm = new MyChild();
pm->f(); // Aufruf von der Methode des Objekts
338
Beispiel konkret (1):

class MyBase {
public:
void f() {cout << "Basis" << endl;};
};

class MyChild : public MyBase {


public:
void f() {cout << "Child" << endl;};
};

int main(){
MyBase m;
MyChild c;
MyBase * pm = &m;
MyChild * pc = &c;

pm->f(); Basis
pm = pc;
pm->f();
Basis
}

339
Beispiel konkret (2):

class MyBase {
public:
virtual void f() {cout << "Basis" << endl;};
};

class MyChild : public MyBase {


public:
void f() {cout << "Child" << endl;};
};

int main(){
MyBase m;
MyChild c;
MyBase * pm = &m;
MyChild * pc = &c;

pm->f(); Basis
pm = pc;
pm->f();
Child
}

340
Vergleich C++ und Java

 Java
 Alle Methoden werden zunächst dynamisch gebunden!
 Methoden können durch das Schlüsselwort final zu statisch
gebundenen Methoden werden.
 In Java gibt es nur Referenzvariablen.
 C++
 Alle Methoden werden zunächst statisch gebunden.
 Methoden können durch das Schlüsselwort virtual zu dynamisch
gebundenen Methoden werden.
 Dynamisch gebunden wird nur bei Zeiger- und Refernzvariablen

341
Abstrakte Basisklassen

 Ausgehend von der Klasse GrafikObjekt haben wir Klassen für Kreise,
Rechtecke und Quadrate definiert.

GrafikObjekt

GRechteck GKreis

GQuadrat
 Oft wird die Basisklasse nicht für die Erzeugung von Objekten benötigt,
sondern nur für die Deklaration von Variablen.
 Beispiel: Verwaltung einer Liste von grafischen Objekten.
 In einem solchen Fall spricht man von einer abstrakten Klasse.
 Typisch für diese sind Methoden, die nicht vollständig implementiert sind.
 Beispiel:
 Die Methoden zeichne und loesche sollen nicht in der Basicklasse
implementiert werden, sondern müssen aber in den Unterklassen definiert
werden.

342
GrafikObjekt als abstrakte Basisklasse (1)

 Durch " = 0 " wird per Definition zeichne und loesche zu einer abstrakten
Methode und damit GrafikObjekt zu einer abstrakten Klasse.
class GrafikObjekt{
private:
int xpos;
int ypos;
public:
GrafikObjekt(): xpos(0), ypos(0) {}
GrafikObjekt(int x, int y) : xpos(x), ypos(y) {}
~grafikobjekt();
virtual void zeichne() = 0;
virtual void loesche() = 0;
void setPos(int x, int y) {xpos = x; ypos = y;}
void verschiebe(int x, int y){
loesche();
xpos += x; ypos += y;
zeichne();
}
};
343
GrafikObjekt als abstrakte Basisklasse (2)

 Eine abstrakte Klasse in C++, ist eine Klasse, die mindestens


eine abstrakte Methode enthält.
 Mit abstrakten Klassen können keine Instanzen gebildet werden.
 Eine abstrakte Methode in C++ ist virtuell und dadurch
gekennzeichnet, dass ihre Deklaration durch " = 0; "
abgeschlossen wird.
 Eine abstrakte Methode kann jedoch außerhalb der Klasse wie
üblich noch implementiert sein.
 Sinn abstrakter Klassen ist, dass ihre abstrakten Methoden in
abgeleiteten Klassen reimplementiert werden müssen, um dort
eine Instanzenbildung zuzulassen.
 Anwendungsmöglichkeiten für abstrakte Klassen sind z.B. das
definierte GrafikObjekt, welches Grundfunktionen und -felder von
Grafikobjekten bereitstellt.

344
GrafikObjekt als abstrakte Basisklasse (3)

 Abstrakte Klassen verwendet man auch gerne um Schnittstellen zu


definieren. Unsere grafischen Objekte haben jeweils eine Fläche.
 Es liegt also nahe eine Funktion dafür in der Basisklasse zu deklarieren:
virtual double flaeche() const = 0;

 Diese muss dann für Rechtecke und Kreise definiert werden, für
Quadrate ist das nicht nötig…
double flaeche() const { return breite*hoehe;}

double flaeche() const {


return return PI*radius*radius;}

 Beispiel:
double sum = 0;
GrafikObjekt *f[] ={new Rechteck(7,8,9,1),
new Kreis(4,5,6), new Quadrat(1,2,3)};
for (int i=0; i < 3; i++) sum += f[i]->flaeche();
cout << "Gesamtflaeche: " << sum << endl;
345
Vergleich Java und C++

 Java
 Abstrakte Klassen werden durch das Schlüsselwort abstract
gekennzeichnet.
 Abstrakte Methoden besitzen keinen Rumpf.
 C++
 Abstrakte Klassen besitzen mindestens eine abstrakte Methode.
 Abstrakte Methoden können einen Rumpf besitzen.

 Empfehlung für abstrakte Klasse in C++


 Der Destruktor sollte abstrakt deklariert werden.

346
Mehrfachvererbung

 Im Gegensatz zu Java bietet C++ auch die Möglichkeit, eine


Klasse von mehreren Basisklassen abzuleiten.
 Syntax
class HausBoot : public Haus, public Boot {…}

 Die Vererbung aus den einzelnen Basisklassen lässt sich


durch public, private und protected markieren.
 Problem der Mehrfachvererbung durch Mehrdeutigkeiten
 Besitzen z. B. die Klassen Haus und Boot die Methode getLength(),
so führt
HausBoot hb(…);
hb.getLength();

zu einem Fehler.´In diesem Fall hilft der Scope-Operator:

hb.Haus::getLength(); // ruft die Methode aus der Klasse Haus


hb.Boot::getLength(); // ruft die Methode aus der Klasse Boot
347
Rautenförmige Mehrfachvererbung

 Zwar ist eine zweimalige Vererbung von einer Klasse nicht erlaubt, aber
das Erben aus einer Basisklasse kann indirekt mehrfach erfolgen:

Konto
SparKonto GiroKonto

Konsequenz KombiKonto
 Die Elemente der Klasse Konto sind zweimal in der Klasse KombiKonto
vorhanden. Auch hier kann durch den Scope-Operator erstmal geholfen
werden:
kto.GiroKonto::getNr(); // ist ok

 Trotzdem sollte vermieden werden, dass die gleichen Komponenten


mehrmals auftreten.
 Verwendung einer so genannten virtuellen Vererbung:

class GiroKonto: public virtual Konto;


348
Rautenförmige Mehrfachvererbung

 Initialisierung von Basisklassen


 Da Mehrfachinitialisierung einer Klasse wie z. B. Konto nicht erlaubt
sind, darf in unserem Beispiel die Initialisierung erst in der Klasse
KombiKonto erfolgen (und nicht in GiroKonto und SparKonto).
 Ein Problem der Mehrfachvererbung
 Wenn noch weitere Vererbungen existieren, ist die Verbung
zwischen Konto und SparKonto auf virtual public gesetzt.

virtual public Konto virtual public

SparKonto GiroKonto

KombiKonto

RatenSparKonto
349
Vergleich Java und C++

 Java
 Mehrfachvererbung ist in dieser Form in Java nicht möglich.
 Als Ersatz für die Mehrfachvererbung gibt es Schnittstellen.
 Eine Klasse kann mehrere Schnittstellen implementieren.
 C++
 Mehrfachvererbung von Klassen wird unterstützt.
 Schnittstellen gibt es nicht.
 Ähnlich wie bei Schnittstellen in Java ist Mehrfachvererbung auch
ein sinnvolles Instrument.

350
Operatoren

 Fallstudie: Die Klasse Complex


 Befreundete Methoden und Klassen
 Überladen eines Operators
 Regeln für das Überladen
Beispiel: Klasse Complex
 Wir definieren zunächst als Beispiel eine Klasse für komplexe Zahlen:

class Complex {
double re , im;
public:
Complex(double r = 0.0, double i = 0.0) { re = r; im = i;}
void add (const Complex& z){ re += z.re; im += z.im; }
void show (){ cout << "Realteil: " << re
<< " Imaginärteil: " << im << endl;}
};

 Die Methode add addiert zu der vorhandenen Instanz eine andere


komplexe Zahl hinzu.
 Anwendung: Complex a(1,2), b(3,4);
a.add(b);
b.add(a);
a.show();
b.show(); Realteil: 4 Imaginärteil: 6
Realteil: 7 Imaginärteil: 10
352
Problem: Definition globaler Funktionen

 Man kann so mit komplexen Zahlen rechnen, aber natürlicher wäre


folgende Schreibweise:
Complex a(1,2), b(3,4), c;
c = add(a,b);

 Das heißt wir wollen eine globale Funktion add definieren, die komplexe
Zahlen addieren kann:
Complex add (const Complex& z1, const Complex& z2){
Complex k;
k.re = z1.re + z2.re;
k.im = z1.im + z2.im;
return k;
};

 Das geht so aber nicht, da wir in einer globalen Funktion nicht auf die
privaten Felder der Klasse Complex zugreifen können.

353
Deklaration befreundeter Methoden (1)

 Man kann aber add als Freund der Klasse Complex definieren.
 Dies müssen wir bereits in der Klasse Complex deklarieren.
 Die Implementierung kann allerdings nachgeholt werden:
class Complex {
double re , im;
public:
friend Complex add (const Complex& z1, const Complex& z2);
void show (){ cout << "Realteil: " << re
<< " Imaginärteil: " << im << endl;
}
};
Complex add(const Complex& z1, const Complex& z2){
return Complex (z1.re + z2.re, z1.im + z2.im);
};

Complex a(1,2), b(3,4), c;


c = add(a, b);
c.show(); Realteil: 4 Imaginärteil: 6
354
Deklaration befreundeter Methoden (2)

Alternative
 Man kann aber add als Freund der Klasse Complex deklarieren und gleich die
Implementierung hinzufügen:

class Complex {
double re , im;
public:
friend Complex add (const Complex& z1, const Complex& z2){
return Complex (z1.re + z2.re, z1.im + z2.im);
}
void show (){ cout << "Realteil: " << re
<< " Imaginärteil: " << im << endl;
}
};
Complex a(1,2), b(3,4), c;
c = add(a, b);
c.show(); Realteil: 4 Imaginärteil: 6

355
Befreundete Methoden und Klassen

 Befreundeten Methoden gestattet man den Zugriff auf private


Felder ....
 Befreundete Methoden können global deklariert sein.
 Befreundete Methoden können aus anderen Klassen sein.
 friend int YourClass::method1(MyClass& obj);
 Aber auch ganze Klassen können befreundet sein, dann sind alle
Methoden dieser Klassen befreundet.
 friend class FClass;

356
Beispiel: Überladen eines Operators
 Die bisher erreichte Schreibweise ist ja ganz nett, aber schöner wäre
Complex a(1,2), b(3,4), c;
c = a + b;

 In C++ kann man in der Tat Operatoren überladen.

class Complex {
double re , im;
public:
Complex(double r = 0.0, double i = 0.0) { re = r; im = i;}
friend Complex operator+(const Complex& z1,const Complex& z2);
void show (){ cout << "Realteil: " << re
<< " Imaginärteil: " << im << endl;
}
};

Complex operator+ (const Complex& z1, const Complex& z2){


return Complex (z1.re + z2.re, z1.im + z2.im);
};
357
Operator als Objektmethode
 Den gleichen Effekt hätten wir übrigens gehabt, wenn wir den Operator
nicht global sondern als Objektmethode definiert hätten:
class Complex {
double re , im;
public:
Complex(double r = 0.0, double i = 0.0) { re = r; im = i;}
Complex operator+ (const Complex& z){
return Complex (re + z.re, im + z.im); Ein Argument!
};

 Oder genau so gut:


class Complex {
double re , im;
public:
Complex(double r = 0.0, double i = 0.0) { re = r; im = i;}
friend Complex operator+(const Complex& z1,const Complex& z2){
return Complex (z1.re + z2.re, z1.im + z2.im);};
};
Zwei Argumente!
358
Beispiel: Klasse Complex (7)
 In den Beispielen haben wir jeweils ein Objekt als Ergebnis geliefert. In einigen
Fällen macht das jedoch keinen Sinn.
 Angenommen wir wollen den Operator += für komplexe Zahlen definieren, und
zwar nicht als globale Funktion sondern als Elementfunktion, dann möchte man
"sich selbst" zurückgeben:

class Complex {
double re , im;
public:
Complex(double r = 0.0, double i = 0.0) { re = r; im = i;}
Complex& operator+= (const Complex& z){
re = re + z.re;
im = im + z.im;
return *this;
}
};

Complex a(1,2), b(3,4);


a += b;
a.show(); Realteil: 4 Imaginärteil: 6
359
Operatoren

 Wie in den Beispielen gezeigt, kann man in C++ neue Operatoren definieren.
 Es gibt nur wenige Unterschiede zu der Definition einer Methode.
 Statt eines Methodennamens benutzt man das Schlüsselwort operator
gefolgt von dem gewünschten Operator.
 Zwei Möglichkeiten für das Überladen eines Operators
 Globale Methode
 Parameter werden dann wie bei einer Methode definiert,
 Objektmethode
 Das Objekt ist dann implizit der erste Parameter des Operators
 Beim Aufruf des Operators bestimmt der erste Operand, welche
Implementierung genutzt wird.

360
Definition eines Operators

Durch diese Definition wird ein + Operator für die Klasse CDatum
definiert : CDatum operator +(const CDatum& d, int i) {
CDatum erg(d);
erg.zeit += i;
erg.berechneDatum();
return erg;
}

+: CDatum int CDatum

Anwendung:
CDatum xDatum; Was wird eigentlich
CDatum meinDatum(2004,2,29,23,57); addiert ?
meinDatum.zeige();
xDatum = meinDatum + 3;
xDatum.zeige();
361
Klassen und ihre Freunde (1)

 Ganz so einfach geht es allerdings leider nicht, da der soeben definierte


Operator private Felder und Methoden der Klasse CDatum benutzt.
 Um dies zu ermöglichen, muss diese Operatordefinition daher in der
Klassendefinition "angemeldet" werden, obwohl der zu definierende
Operator nichts direkt mit einem speziellen Objekt dieser Klasse zu tun hat.
 Er muss aber als "Freund" dieser Klasse angemeldet werden
 siehe auch das Beispiel "Complex"
 Befreundete Methoden dürfen auf private Elemente zugreifen.
 Die folgende Zeile muss daher in der Definition der Klasse CDatum
auftauchen:

friend CDatum operator + (CDatum& d, int i);

362
Klassen und ihre Freunde (2)
 In vielen Fällen will man aber ein Datum inkrementieren
(dekrementieren).
 Im Unterschied zu den bisherigen Operatoren, verhalten sich die
zu definierenden eher wie Methoden, da sie das Objekt indem sie
definiert sind, verändern.
 Wir könnten die Inkrement-Operationen wahlweise als friend- oder
als Methoden definieren.
 Wichtig: Das Objekt ist dann implizit der erste Parameter.
 Das folgende Beispiel definiert einen Operator als Methode.
In der Klassendefinition: CDatum& operator +=(int i);

Implementierung: CDatum& CDatum::operator +=(int i){


zeit += i;
berechneDatum();
return * this;
}

363
Notwendigkeit von friend

 Der erste Operand bestimmt die zugehörige Klasse des Operators.


 Manchmal kann aber der erste Operand nicht von der gewünschten Klasse
sein.
 Folgende Beispiele illustrieren deshalb die Notwendigkeit der friend-
Deklaration beim Operatorüberladen.
 Objekt der Klasse ist der zweite Operator:
friend ostream& operator << (ostream& os, const CDatum& d);

 Bewahrung der Symmetrie von Operatoren:


friend CDatum operator + (CDatum& d, int i);
friend CDatum operator + (int i, CDatum& d);

 Es gibt aber auch wieder einmal Ausnahmen!


 Die Operatoren =,(),[] und  dürfen nicht mit friend überladen werden.

364
Überladungsfähige Operatoren
 Nahezu alle Operatoren können überladen werden:

+ - * / % ^ & | ~ !

= < > += -= *= /= %= ^= &=

|= << >> >>= <<= == != <= >= &&

|| ++ -- ->* , -> [] () new delete

 Folgende Operatoren sind nicht überladbar:


 sizeof, ::, ., ?:
 Überladene Operatoren werden auch vererbt.
 Ausnahme: = wird nicht an andere Klassen vererbt.

365
Weitere Operatoren

friend bool operator == (const CDatum& d1, const CDatum& d2);


friend bool operator != (const CDatum& d1, const CDatum& d2);
friend ostream& operator << (ostream& os, const CDatum& d);

bool operator == (const CDatum& d1, const CDatum& d2){


== return d1.zeit == d2.zeit;
}

bool operator != (const CDatum& d1, const CDatum& d2){


!= return d1.zeit != d2.zeit;
}

ostream& operator << (ostream& os, const CDatum& d){


os << " Jahr: " << d.jahr
<< " Monat: " << d.monat
<<
<< " Tag: " << d.tag
<< " Stunde: " << d.stunde
<< " Minute: " << d.minute;
return os;
}

cout << meinDatum << endl;


366
Inkrementieren
Natürlich kann man auch ++ als Operator neu definieren:
CDatum& operator ++(int); // d++ Postfix Version !
Deklaration
CDatum& operator ++(); // ++d Präfix Version !

CDatum CDatum::operator ++ (int){ // d++ Postfix


CDatum tmp(*this);
zeit++;
berechneDatum();
return tmp;
Implementierung: }

CDatum& CDatum::operator ++ (){ // ++d Präfix


zeit++;
berechneDatum();
return * this;
}

Nachteil: Schwierig ist i. A. die Postfix Variante!


Wegen dieser Probleme sollte man ++ Operatoren möglichst nicht
überladen.
367
Zuweisungsoperator

Wenn man einen Zuweisungs-Operator definiert, kann man


auch gleich einen Copy-Konstruktor definieren!

Umgekehrt: Wenn man einen Copy-Konstruktor benötigt,


sollte man vorher einen Zuweisungs-Operator definieren !!

CDatum(const CDatum& d){ *this = d;}


In der Klassendefinition:
CDatum& operator = (const CDatum& op);

CDatum& CDatum::operator = (const CDatum& op){


zeit = op.zeit;
Implementierung: berechneDatum();
return *this;
}

368
Überladen von new und delete
 Anpassung der Speicherorganisation durch ein geeignetes Überladen von new
und delete
 schnellere an die Hardware angepasste Speicherallokation
 verbesserte Kontrolle und Fehlersuche
 klassenspezifische new-Operatoren mit einer eignen Garbagecollection.
 Lokales Überladen in einer Klasse vs. globales Überladen für alle Klassen
 Einfachste Form:
void* operator new(size_t);
void operator delete(void *p);
 Beim lokalen Überladen sind new und delete Bestandteil (Klassenmethoden)
einer Klasse.
 Beim globalen Überladen werden diese außerhalb einer Klasse definiert.
 Im Zweifelsfall wird die lokal überladene Funktionalität genutzt.

369
Überladen von new und delete bei Arrays

 Möchte man ein Array von Objekten speziell allozieren, muss


noch ein weiterer Operator überladen werden.
 Syntax der Operatoren
 void *operator new[] (site_t size);
 void operator delete[] (void *p);

370
Wichtige Regeln für Operatoren (1)

 Die Menge der möglichen Operatoren ist auf die in C++


vorhandenen beschränkt, d.h. es ist z.B. nicht möglich, einen neuen
Operator ** für das Potenzieren zu definieren.
 Die üblichen Prioritätsregeln haben weiterhin Gültigkeit, können also
insbesondere auch nicht neu definiert werden.
 Es ist z. B. für die Klasse Complex nicht möglich, dem
Additionsoperator eine höhere Priorität zuzuweisen als dem
Multiplikationsoperator.
 Die Stelligkeit eines Operatores kann ebenfalls nicht geändert
werden, d.h. es kann nicht vom Programmierer festgesetzt werden,
ob ein bestimmter Operator unär oder binär ist.
 Ein unäres % oder ein binäres ! sind demnach nicht möglich.
 Die Operatoren der vordefinierten Datentypen sind festgelegt und
können nicht überschrieben werden.

371
Wichtige Regeln für Operatoren (2)

 Die von anderen Operatoren verwanden Operatoren werden nicht


automatisch bei benutzerdefinierten Klassen zur Verfügung gestellt.
 So ist beispielsweise für unser Beispiel der Klasse Complex,
wenn wir + und = definiert haben, nicht automatisch += definiert.
 Der Programmierer hat darauf zu achten, dass bestimmte
"erwartete" Operatoren (etwa der Operator *=, wenn bereits die
einzelnen Operatoren für die Multiplikation und Zuweisung
vorhanden sind) bereitgestellt werden.

372
Operatoren: Resümee

 Schwierigkeiten:
 Bedeutung der Operatoren entspricht in einigen Fällen nicht der
Intuition
 Globale Funktionen vs. Methoden
 Friends
 Zuweisungsoperatoren, Copy-Konstruktoren
 Typanpassung
 ++
 ...

373
Ein- und Ausgabe Ströme

 Streams
 Methoden von ostream, ofstream, istream,
ifstream
 Zustand von Strömen
 Manipulatoren und Formatkontrolle
 Beispiel: Kopieren einer Datei
Streams
 Von Anfang an haben wir Ein- und Ausgabeoperationen für Streams
angewendet.
 Bereits früher haben wir die Grundlagen hierzu diskutiert.
 Weitere Details:
 Auch Streams sind durch Klassen definiert.
 Die Operationen << und >> definieren überladene Operatoren im Sinne des
letzten Kapitels.
 Dort haben wir auch ein Beispiel gesehen, wie man für einen eigene Klasse
diese Operatoren überladen kann:

ostream& operator << (ostream& os, const CDatum& d){


os << " Jahr " << d.Jahr
<< " Monat " << d.Monat
<< " Tag " << d.Tag
<< " Stunde " << d.Stunde
<< " Minute " << d.Minute;
return os;
}

375
Klassenhierarchie
 Die Klasse istream ist ein Eingabestrom (input stream), von dem Daten
gelesen werden können.
 Die Klasse ostream ist ein Ausgabestrom (output stream), auf den Daten
ausgegeben werden können.
 Die beiden genannten Klassen besitzen in der Klasse ios eine gemeinsame
Basisklasse, die spezielle Eigenschaften aller Stream-Klassen definiert, wie
etwa Status-Flags und Methoden, um diese Flags zu setzen oder
abzufragen ios_base

ios

istream ostream

istringstream ifstream iostream ofstream ostringstream

fstream stringstream
376
Der << Operator

int i = 42;
cout << "Teststring" << i;

 An dem Beispiel wird deutlich, das es sich bei cout um ein Objekt der
Klasse ostream handelt und das der Operator << in dieser Klasse unter
anderem folgende überladene Definitionen erhalten muss:

ostream& operator << (const char*);


ostream& operator << (const int);

 Dabei ist der Operator derart implementiert, dass jeweils der zweite
Operand in den Stream ausgegeben wird.
 Der erste Operand dieses Operators, also das Stream-Objekt, wird nach
der Ausgabe zur weiteren Verwendung zurückgegeben.

377
Methoden von ostream
 Für die Klasse ostream stehen neben dem Ausgabeoperator << weitere
Methoden zur Verfügung. Beispiele:
 ostream& put (char c): Gibt ein Zeichen aus.
 ostream& write (char* s, int anz) : Gibt anz Zeichen des
Strings s aus.
 ostream& flush () : Leert den Ausgabepuffer - alle darin
stehenden Zeichen werden ausgegeben.
 ostream& endl () : fügt ein Zeilenende zur Ausgabe hinzu und
ruft dann flush() auf.
 Benutzt werden
diese Methoden char c = 'c';
zum Beispiel wie folgt: char* str = "abcdefghijklmnopqrst";
...
cout.put(c);
cout.write(str, 10);
cout.endl();
378
Methoden von istream (1)

 Für die Klasse istream stehen neben dem Eingabeoperator >> weitere
Methoden zur Verfügung.
 Anwendung von >>: cin >> ch;
 Beispiele:
 int get() : Liefert das nächste gelesene Zeichen oder EOF zurück
 istream& get(char& c) : Weist das nächste Zeichen dem
übergebenen Parameter c zu und liefert den Stream zurück;
 istream& get(char* s, int anz, char ende = '\n') :
liest bis zu anz Zeichen in den String s ein
 spätestens mit dem Zeichen ende wird abgebrochen
 das Zeichen ende wird nicht mehr übernommen.
 istream& getline(char* s, int anz, char ende='\n') :
Analog zur get-Funktion, jedoch wird nun das Zeichen ende mit
übernommen.
379
Methoden von istream (2)
 Für die Klasse istream stehen neben dem Eingabeoperator >> weitere
Methoden zur Verfügung. Beispiele:
 istream& read(char* s, int anz) : Liest einfach bis zu anz
Zeichen in den String s ein
 int gcount() : Liefert die Anzahl der Zeichen beim letzten
Lesebefehl zurück.
 istream& ignore(int anz = 1, int ende = EOF) :
Überliest maximal anz Zeichen, bis ende - Zeichen oder EOF auftritt.
 int peek() : Liefert das nächste Zeichen aus dem Stream oder
EOF, ohne es auszulesen.
 istream& putback(char c) : Stellt das Zeichen c in den
Eingabe-Stream zurück, damit es beim nächsten Mal erneut gelesen
wird.

380
Zustände von Strömen
 In der Klasse ios sind Flags definiert, die den Zustand von Strömen
anzeigen. neben dem Ausgabeoperator >> weitere Methoden zur
Verfügung. Beispiele:
 ios::goodbit alles in Ordnung
 ios::eofbit End-Of-File (Ende der Datei)
 ios::failbit Fehler: letzter Vorgang nicht korrekt abgeschlossen
 ios::badbit fataler Fehler: Zustand nicht definiert
 ios::hardfail Hardware-Fehler
 Natürlich sollte man diese Flaggen nicht direkt lesen oder gar schreiben.
Dafür gibt es folgende Methoden:
 good( ) alles in Ordnung (ios::goodbit gesetzt)
 eof( ) End-Of-File (ios::eofbit gesetzt)
 fail( ) Fehler (ios::failbit oder ios::badbit gesetzt)
 bad( ) Fataler Fehler (ios::badbit)
 clear(int flags = 0) löscht bzw. setzt die Flags, falls der
Parameter ungleich 0 ist.
381
Manipulatoren
cout << endl ; hatten wir bereits kennengelernt.

 Auch Ausgabeströme haben Manipulatoren.


 Z.B. ws (Bedeutung: ignore whitespaces).
 Einige der Manipulatoren sind:
dec, endl, ends, flush, hex, oct, (no)boolalpha,
(no)showbase,(no)showpoint, (no)showpos, ….
resetiosflags(long f), setbase(int base),
setfill(int ch), setiosflags(long f),
setprecision(int p), setw(int w), ws …

 Für einige Manipulatoren kann der Aufruf auch als Methodenaufruf


erfolgen: cout.endl();
cout.fill(ch);
cout.width(n);

382
Manipulatoren: Beispiele
int main(){
int i = 3, k =5;
cout << noboolalpha << "Boolean ohne boolalpha: " ;
cout << (i == k) << " " << (i != k) << endl;
cout << boolalpha << "Boolean mit boolalpha: " ;
cout << (i == k) << " " << (i != k) << endl;
}
Boolean ohne boolalpha: 0 1
Boolean mit boolalpha: false true
int main(){
long long cb = 0Xcafebabe, ca = 0XC0B0L;
cout << oct << cb << " " << ca << endl;
cout << hex << cb << " " << ca << endl;
cout << dec << cb << " " << ca << endl;
cout << showbase << "mit Show-Base : " << endl;
cout << oct << cb << " " << ca << endl;
31277535276 140260
cout << hex << cb << " " << ca << endl;
cafebabe c0b0
cout << dec << cb << " " << ca << endl; 3405691582 49328
} mit Show-Base :
031277535276 0140260
0xcafebabe 0xc0b0
383
3405691582 49328
Definition von Manipulatoren

 Wir hätten endl auch selbst definieren können:


ostream& endLine (ostream& os) { return os << '\n' << flush; }

 Wir können generell auch eigene Manipulatoren definieren :


ostream& tab (ostream& os) { return os << '\t' ; }
ostream& bell (ostream& os) { return os << '\a' ; }

 Testprogramm:
int main(){
cout << "Demonstration von endLine" << endLine;
cout << "Demo Tab:" ;
for (int i=0; i < 7; i++) cout << i << tab;
cout << endl << "Demo Klingel:" ;
for (int i=0; i < 142; i++) cout << bell;
cout << endl;
}

384
Formatkontrolle
 Zur Formatkontrolle sind in ios folgende Konstanten definiert:
ios::left, ios::right, ios::internal,
ios::oct, ios::dec, ios::hex, ios::basefield,
ios::showbase, ios::showpoint, ios::showpos,
ios::fixed, ios::scientific, ios::uppercase,
ios::skipws, ios::unitbuf, ios::stdio
 Beispiel:
int wert=2044;
long alte_basis;
cout.setf(ios::oct, ios::basefield);
cout << "Der Wert betraegt: " << wert << endl;// Ausgabe octal
// alte Zahlenbasis speichern und Umschalten auf hexadezimale
Darstellung
alte_basis= cout.setf( ios::hex, ios::basefield);
cout << "Der Wert betraegt: " << wert << endl;// Ausgabe hexadezimal

// alte Einstellugen wiederherstellen


cout.setf(alte_basis, ios::basefield);
cout << "Der Wert betraegt: " << wert << endl;// Ausgabe octal
385
ofstream
 Diese Klasse erlaubt das Schreiben in Dateien.
 Das Eröffnen einer Datei erfolgt ggf. durch den Konstruktor:
ofstream MeineDatei("neu.dat");

 Nach dem Öffnen sollte man Testen ob die Datei erfolgreich geöffnet
wurde:
if ( ! MeineDatei) { Fehler }
else { normal weiter };

 Default ist das Öffnen zum Überschreiben, also:


ofstream MeineDatei("neu.dat", ios::out);

 Alternativ kann zum Erweitern geöffnet werden:


ofstream MeineDatei("erw.dat", ios::app);

 Schreiben in eine Datei: wie bei cout - also z.B.


MeineDatei << "Erste Zeile" << endl ;

386
ifstream
 Diese Klasse erlaubt das Lesen aus Dateien.
 Das Eröffnen einer Datei erfolgt ggf. durch den Konstruktor:
ifstream MeineDatei("neu.dat");

 Nach dem Öffnen sollte man Testen ob die Datei erfolgreich geöffnet
wurde: if ( ! MeineDatei) { Fehler }
else { normal weiter };

 Default ist das Öffnen zum Lesen, also:


ifstream MeineDatei("neu.dat", ios::in);

 Lesen aus einer Datei: wie bei cin - also z.B.


MeineDatei >> s ;

387
Beispiel: Kopieren einer Datei (1)
#include <iostream>
#include <fstream>
using namespace std;
int main( void ){
ifstream Quelle ( "fileioX1.cpp", ios::in);
if ( !Quelle ) {
Quelle
cout << "Die Datei 'fileioX1.cpp'"
<< "konnte nicht geöffnet werden." << endl;
return;}
else
cout << "Die Datei 'fileioX1.cpp' wurde geöffnet.\n" ;
ofstream Ziel ( "fileioX1.bak", ios::out);
if ( !Ziel ){
Ziel
cout << "Die Datei 'fileioX1.bak'"
<< "konnte nicht geöffnet werden." << endl;
return;}
else
cout << "Die Datei 'fileioX1.bak' wurde geöffnet.\n" ;
...
388
Beispiel: Kopieren einer Datei (2)

...
char ch ;
while( Quelle.get(ch) ) Ziel.put(ch);
if (!Quelle.eof() || Ziel.bad() )
cout << "Fehler aufgetreten.\n";
Kopieren...
else cout << "Alles paletti..\n";
Quelle.close();
Ziel.close();
}

389
Exceptions

 Motivation
 Idee und allgemeine Form von Exceptions
 Werfen von Exceptions
 Behandlung von Exceptions
 Mehrere catch-Anweisungen und Vererbung
 Optionen der Behandlung von Exceptions
 Vergleich von Exceptions in C++ und Java
Motivation

 Bei vorgesehener Funktionalität eines Programms können


Dinge auftreten, die vom Programmierer als unnormal
betrachtet werden.
 Bsp. : Daten in Datei abspeichern
Pseudocode:
Erzeuge Datei mit gewünschten Namen;
Schreibe Inhalt;
Schließe Datei;
 Mögliche Probleme
 Dateiname ist nicht zugelassen
 Datei existiert schon
 Ungenügend Rechte zum Erstellen der Datei
 Platz reicht nicht für das Speichern aus
 Datei ist von einem andren Prozess blockiert
…

391
Motivation(2)

 Unbehandelte Probleme können zum Programmabsturz führen

 Berücksichtigung aller Probleme …


 mit if / else führt zu unübersichtlichen Programm
 führt dazu, dass Programmierer sich ständig mit
„Nebensächlichkeiten“ beschäftigen muss und somit von dem
eigentlichen Problem abgelenkt wird.
 Wer findet schon alle Probleme, die in einem Programm auftreten
können?

392
Idee und allgemeine Form von
Exceptions

 Idee
 Schreibe Code offensiv und behandle Fehler hinterher, falls etwas
nicht so klappt wie vorgesehen
 Allgemeine Form
try
{ /* tryblock */ } // Programmlogik
catch (type1 arg) { /* catch block type 1 */ }

catch (typen arg) { /* catch block type n */ }

 Bedeutung
 Exceptions in Java und C++ sind sich sehr ähnlich
 Gemeinsamkeiten und Unterschiede am Ende des Kapitels

393
Werfen von Exceptions

 Werfen von Exceptions geschieht mit dem Schlüsselwort throw


gefolgt von einem Ausdruck
 In C++ kann der Typ des Ausdrucks beliebig sein

 Beispiele
 throw 1; // Exception vom Typ int
 throw "Fehler"; // Exception vom Typ const char*
 throw new A(); // Exception vom Typ
// der selbstdef. Klasse A

 Die Verarbeitung von Exceptions ist aufwändig und soll nicht für
erwartete Ereignisse missbraucht werden
 Bsp: Lesen des Ende einer Datei

394
Behandlung von Exceptions

 Behandlung wie von Java gewohnt im try / catch Block


try {
if (i < 0) throw -1;
if ( m!= 19) throw "Mehrwertsteuer falsch
initialisiert";
}
catch (int i) { /* Fehlerbehandlung*/ }
catch (const char* c) { m = 19; }

 Geworfene aber nicht gefangene Exceptions führen zum


Abbruch der aktuellen Funktion
 Wird eine Exception in der Funktion main nicht behandelt kommt es
zum Abbruch des Programms (Details später)

395
Mehrere catch Anweisungen und
Vererbung

 Oft wird beim Werfen einer Exception ein Objekt einer Klasse
erzeugt (statt const char* besser Klasse
 Bei der Behandlung der Exception sollten wie in Java die
speziellsten Unterklassen zuerst behandelt werden.
 Beispiele
 A Oberklasse von B und C
 Beispiel 1
try { /* Anweisungen…*/ }
catch(A a) { /* Behandlung für A, B, und C */ }
// separate Behandlung von B und C nicht mehr möglich
 Beispiel 2
try { /* Anweisungen…*/ }
catch(B a) { /* Behandlung für B */ }
catch(A a) { /* Behandlung für A und C */ }

396
Optionen für die Behandlung
von Exceptions

 Übersicht
 Fangen beliebiger Exceptions
 Exeptions beschränken
 Exceptions erneut auslösen
 Die Funktionen terminate() und unexpected()

397
Fangen beliebiger Exceptions

 Da es in C++ keine Oberklasse aller Exceptions und Fehler


gibt (siehe Throwable in Java), muss es eine Möglichkeit
geben, alle geworfenen Typen abzufangen:
 catch(…) fängt alles ab, was vorher noch nicht behandelt
wurde.

 Beispiel
try { /* Anweisungen…*/ }
catch (A a){ /* Behandlung für Typ A */ }

catch(…) { /* Behandlung für übrige Typen */ }

398
Exceptions beschränken

 Analog zu Java können die Exceptions, die eine Funktion werfen


darf, beschränkt werden.
 Dies geschieht in C++ ebenfalls mit dem Schlüsselwort throw
 Allgemeine Form
ret_type function_name (arg_list) throw(type_list){…}
 Beispiel
float divide (float divident, float divisor) throw(std::string) {
if (divisor == 0)
throw std::string("division by zero not allowed");
return divident / divisor;
}
 Ohne Angabe einer throw-Klausel sind alle Exceptions zugelassen
 Wird eine Funktion durch eine nicht angegebene Exception beendet führt
dies zum Programmende (Details später)

399
Exceptions erneut auslösen

 Innerhalb eines catch-Blocks kann die gleiche Exception


erneut geworfen werden. Dies geschieht durch Aufruf von
throw ohne Argument.
 Der aktuelle try-catch-Block wird verlassen und es wird die Exception
dann im nächsten try-catch-Block behandelt.
 Beispiel
try { /* Anweisungen…*/ }
catch(A a) {
// Fehlerbehandlung, soweit lokale sinnvoll
throw; // weitere Fehlerbehandlung außerhalb
// entspricht „throw a;“
}

400
Die Funktionen terminate() und
unexpected()

 Exceptions können zum Programmende führen


 Nichtgefangene Exceptions führen zum Aufruf von terminate().
 Geworfene, nicht im Funktionskopf erlaubte Exceptions führen zum
Aufruf von unexpected().
 Defaultverhalten dieser Funktionen: ( = Aufruf)
 unexpected()  terminate()
 terminate()  abort()
 abort()  sofortiger Abbruch des Programms
 Die Funktionen terminate() und unexpected() sind Teil der
Standartbibliothek von C++.
 terminate() und unexpected() können durch setter-
Methoden (und Verwendung von Funktionsvariablen)
geändert werden.
 Möglichkeit, etwas vor Beendigung auf jeden Fall durchzuführen
 Wichtig: Das Programm muss stets beendet werden

401
Die Funktionen terminate() und
unexpected()

 Beispiel
#include <iostream>
#include <exception>
using namespace std;

void MyExceptionHandler() {
// do something before abort();
abort();
}

int main() {
set_terminate(MyExceptionHandler);
throw 1;
}
 Zum Überschreiben muss exception eingebunden werden.
 Der ExceptionHandler muss abort() aufrufen
 Unter Verwendung von g++ passiert das auch, falls der Handler es nicht
macht.

402
Exceptions in der C++-Bibliothek

 Ähnlich zu Java gibt es in C++ eine Klassenhierarchie für


Exceptions
 Die Klasse exception ist dabei die Oberklasse aller Exceptions
 Im Gegensatz zu Java können in C++ jedoch auch Exceptions von
anderen Typen geworfen und behandelt werden!
 Für die Nutzung der Exception-Klassen ist die Einbindung
von exception notwendig.

403
Gegenüberstellung von Exceptions in
C++ und Java

C++ Java
Argumente für throw beliebig Throwable und
abgeleitete Klassen
Fehler / Exceptionhierachie Ja, exception Ja, Throwable
Fangen beliebiger Exceptions X=… X = Throwable bzw.
mit catch (X) X = Exception
Oberklasse fängt Unterklasse Ja Ja
im catch Block
finally in der Sprache Nein Ja
Einschränken der Exceptions throw throws
einer Funktion
Defaultwert Alle erlaubt Alle verboten
(Ausnahme Runtime
Exception + Sub.Kl)

404
Templates und Metaprogrammierung

 Motivation
 Template Funktionen
 Template Klassen
 Metaprogrammierung

405
Motivation (1)

 Viele Algorithmen können fast ohne jede Änderung auf verschiedene


Datenstrukturen angewendet werden!
 Parametrisierte Funktionen erlauben es, die Programmierung dieser
Algorithmen zu vereinfachen.
 Viele Klassen kann man sehr ähnlich für verschiedene Basis-
Datentypen definieren.
 Beispiel: Die Definition einer Klasse für eine Liste ist weitgehend
unabhängig vom Typ der Listenelemente.
 Andere Beispiele:
 Stacks
 Bäume
 Tabellen
 In diesem Kapitel werden wir u.a. eine generische Version einer Liste
angeben und dann als weiteres Beispiel einen Stack definieren.

406
Motivation (2)

 Programmiersprachen sollten elegante Mechanismen besitzen, um


Redundanz beim Programmieren vermeiden zu könnnen.
 In C++ wird dieses Konzept “Template” genannt.
 Eine andere Bezeichnung (z.B. in Java 1.5) ist “Generic”.
 Betrachten wir z. B. das Sortieren eines Arrays von Datenelementen.
 Mit Template-Funktionen kann man beispielsweise Arrays mit
einem (fast) beliebigen Typ sortieren.
 In vielen Anwendungen ist es nicht nur notwendig eine Funktion,
sondern eine ganze Klasse mit einem Typ zu parametrisieren. Z.B.:
 Implementierung von elementaren Datenstrukturen wie Stack,
Queue, binäre Suchbäume,...
 Hierzu verwendet man Klassen-Templates.

407
Template-Funktionen

template < typename T >


inline void swap (T& a, T& b){
T h(a); a = b; b = h;
}

 Das Schlüsselwort template kennzeichnet swap als Template-


Funktion mit den in spitzen Klammern <...> angegebenen
Parametern
 Beliebig viele Template-Parameter werden durch Kommata separiert
 Template-Parameter sind in nachfolgender Funktionsdefinition
verwendbar
 Sie werden bei Instanziierung zur Compile-Zeit durch konkrete
Parameter (Typen, Werte, Templates) ersetzt

408
Template-Parameter

 Häufigste Form der Parameter: Typ-Parameter


 Angabe durch typename T oder alternativ class T
 T steht stellvertretend für einen konkreten Datentyp (nicht
notwendigerweise eine Klasse!)
Beide Notationen sind äquivalent, typename ist aber evtl. intuitiver

 Jeder gültige C++ Bezeichner kann als Template-Parameter verwendet


werden, üblicherweise werden aber kurze Bezeichner, beginnend ab T
verwendet
 Template-Parameter verdecken class T{ /* … */ };
globale Namen template<typename T> void f()
{ T t; ::T u; }

 Template-Bezeichner können template<typename T> void f(){


in Definition nicht als weitere int T = 0; // Fehler!
Variablen-, oder Klassen- class T{/*...*/}; // Fehler!
Bezeichner verwendet werden …
}
409
Vollständiges Beispiel

Initialisierung einer
template < typename T > Variablen vom Typ T
inline void swap (T& a, T& b) {
mittels Copy-Konstruktur
T h(a);
a = b; Zuweisung von Variablen
b = h;
vom Typ T
}
int main () {
int a(5), b(42);
const char *st1 = "Manfred",
*st2 = "Sommer"; Instanziierung von
cout << a << " " << b << endl;
cout << st1 << " " << st2 << endl;
swap für T = int
swap(a, b);
swap(st1, st2); swap für T = const char*
cout << a << " " << b << endl;
cout << st1 << " " << st2 << endl;
return 0;
} Instanziierung = Instanz - Bildung
410
Template-Instanziierung (1)

 Bei Aufruf von swap(a,b) bzw. swap(st1, st2) findet der


Compiler die passende Schablone swap ...
template < typename T >
inline void swap (T& a, T& b) {
T h(a); a = b; b = h;
}

... und erzeugt folgende Methoden-Instanzen:


inline void swap (int& a, int& b) {
int h(a); a = b; b = h;
}

inline void swap (const char* &a, const char* &b) {


const char* h(a); a = b; b = h;
}

411
Template-Instanziierung (2)

 Alle Template-Parameter werden bei der Instanziierung durch konkrete


Argumente ersetzt.
 Ein konkreter Argument-Datentyp T muss alle Operationen unterstützen,
die das Template verwendet

template < typename T >


inline void swap (T& a, T& b) { T h(a); a = b; b = h; }

class A{ Klasse A mit privatem Copy-Konstruktor


public : A(){}
private: A(const A& a){}
};

int main () {
A a, b; Instanziierung von swap für A führt zu
swap(a,b); Kompilierungsfehler, da A keinen
return 0; öffentlichen Copy-Konstruktor bereitstellt!
}

412
Template-Instanziierung (3)

 Der Compiler erzeugt nur Instanzen für tatsächlich benötigte Funktionen


 Für jede eindeutige Menge von Template-Argumenten wird genau eine
Instanz angelegt
 Implizite Instanziierung durch Aufruf der Funktion im Programm-Code
 Explizite Instanziierung durch Deklaration mit konkretem Typ-Parameter

template < typename T >


inline void swap (T& a, T& b) { T h(a); a = b; b = h; }

template inline void swap<char>(char&, char&);

int main () {
int a, b; Implizite Instanziierung Explizite Instanziierung
swap(a,b); von swap für Typ T = int von swap für Typ T = char
return 0;
}

413
Ein weiteres Beispiel (1)

Insertionsort für alle Daten-


template <typename T>
typen T, die Copy-Konstruktor
void insertionSort (T *arr, int max) {
und die Operationen = und <
for (int k(1); k <= max; k++)
unterstützen
if (arr[k] < arr[k-1]) {
T x(arr[k]);
for (int i(k); ((i > 0) && (x < arr[i-1])); i--)
arr[i] = arr[i-1];
arr[i] = x;
}
} Ausgabe für alle Datentypen T,
die die Operation <<
template <typename T>
unterstützen.
void ausgabe (char *s, T *arr, int max) {
cout << s << endl;
for (int k(0); k < max; k++ )
cout << arr[k] << ",";
cout << arr[max] << endl;
}

414
Ein weiteres Beispiel (2)

int main (){


int arr[] = { 23, 66, 77, 55, 42, 23, 45, 33,
23, 45, 86, 12, 32, 67, 11, 66 };
const int maxAr = (sizeof(arr)/sizeof(arr[0])) - 1;
cout <<"Int-Array mit "<< maxAr+1 <<" Elementen" << endl;
ausgabe("Unsortiert:", arr, maxAr);
1. Sortieren und Ausgeben
insertionSort(arr, maxAr);
eines Integer - Arrays
ausgabe("Sortiert:", arr, maxAr);

char str[] = "DKGDGFJKHFJFGFTERZTZTGEDXJLJKH";


const int maxStr = (sizeof(str)/sizeof(str[0]))-2;
cout <<"Char-Array mit "<< maxStr+1 << " Elementen" << endl;
ausgabe("Unsortiert:", str, maxStr);
insertionSort(str, maxStr); 2. Sortieren und Ausgeben
ausgabe("Sortiert:", str, maxStr); eines char - Arrays
return 0;
}

415
Implizite und explizite Typauswahl

 In den bisherigen Beispielen für Template-Funktionen erfolgte die


Auswahl des Typs T der Schablone implizit durch den Typ der
Argumente, die der Template-Funktion bei der Instanziierung
übergeben wurden. Z.B:
InsertionSort(arr, maxAr);

 Man kann aber auch eine explizite Typangabe für T machen:

InsertionSort<int>(arr, maxAr);

 Man geht dabei davon aus, dass der tatsächlich vorgefundene


Typ zuweisungskompatibel zu dem vorgegeben Typ ist.
 Die explizite Typangabe ist immer dann notwendig, wenn ein
Template-Parameter bei der Instanziierung nicht ableitbar ist

416
Beispiel für explizite Typauswahl (1)

 Template mit mehreren Template-Parametern


 Die Angabe herleitbarer Parameter am Ende der Parameterliste
ist optional
template <typename Out, typename In>
Out implicit_cast(In i){
return i; // Annahme: impliziter Cast In -> Out ist möglich
}

int main () {
int i = 42; Fehler: Rückgabetyp
Out kann nicht ermittelt
// short r = implicit_cast(i); werden!

short s = implicit_cast<short>(i); Out explizit, In implizit


char c = implicit_cast<char, int>(i);
Alle Parameter explizit
return 0;
}

417
Beispiel für explizite Typauswahl (2)

 Typen der Methoden-Argumente müssen exakt mit Template-


Parametern übereinstimmen  keine implizite Typumwandlung
 Explizite Typumwandlung oder Typauswahl erforderlich!
template <typename T>
T max(T a, T b){
return a > b ? a : b;
}
int main () {
int i = 42; Fehler: i und d haben nicht
double d = 3.14; denselben Typ
// int s = max(i, d);  Typauswahl unmöglich

int u = max<int>(i, d);


Explizite Typauswahl
double q = max<double>(i, d);
double r = max( (double) i, d); Explizite Typumwandlung
return 0;  Implizite Typauswahl
}
418
Template-Funktionen: Diskussion

 Die Verwendung von Schablonen für Funktionen erweitert das


Konzept des Überladens von Funktionsnamen.
 Durch die manuelle Definition von Funktionen insertionSort für
verschiedene Array-Datentypen wird schrittweise eine Menge
überladener Funktionsnamen definiert.
 Durch eine Schablone werden auf einen Schlag alle möglichen
überladenen Funktionsnamen definiert!
 Bei der Analyse der Funktionsaufrufe insertionSort(...) zur
Compile-Zeit werden die tatsächlich benötigten Instanzen ermittelt.
 Dabei wird die Kompilation des Unterprogrammes mit dem
gewünschten Typ nachgeholt.
 Dadurch entsteht eine konkrete Instanz der Funktion, die durch die
Schablone abstrakt definiert wurde. Die so entstandene Instanz wird
dann zur Laufzeit für den Funktionsaufruf benutzt.

419
Überladen von Template-Funktionen (1)

 Die durch ein Template definierten Funktionen können weiterhin


überladen werden durch:
 Nicht-Template-Methoden
 Andere Template-Funktion mit gleichem Namen
template< typename T >
T max(T a, T b) {
return a > b ? a : b;
}
max mit Nicht-Template-
int max(int a, int b) {
Funktion überladen
return a > b ? a : b;
}
max mit weiterer Template-
template< typename T >
Funktion überladen
T* max(T* a, T* b) {
return *a > *b ? a : b;
}

Wann wird welche Funktion instanziiert bzw. ausgeführt?

420
Überladen von Template-Funktionen (2)

 Compiler wählt passende Funktion nach folgenden Regeln aus:


(1) Suche Nicht-Template-Funktion mit exakt passender Signatur exakt = ohne impl.
(2) Suche Template-Funktion mit exakt passender Signatur Typumwandlungen
 Bei mehreren passenden Templates wähle das „Spezialisierteste“
 Falls Template-Spezialisierung existiert, wähle Speziellste Implementierung
(3) Suche überladene Nicht-Template-Funktion, die nach impliziter Typumwandlung
eines oder mehrerer Parameter aufrufbar ist

Ein Typ-Parameter T ist spezieller als ein Typ-Parameter S, falls alle


Datentypen, durch die T ersetzt werden kann, auch S ersetzen können,
die Umkehrung aber nicht gilt.

Beispiele:
const T ist spezieller als T
T* ist spezieller als T

421
Überladen von Template-Funktionen (2)

template< typename T >


T max(T a, T b) {
return a > b ? a : b;
Welche Funktionen werden instanziiert und aufgerufen?
}
Welches Ergebnis wird ausgegeben?
int max(int a, int b) {
return a > b ? a : b;
int main(void){
}
int i = 5, j = 42;
template< typename T > cout << max(i,j) << “\n“
T* max(T* a, T* b) { << max<>(i,j) << “\n“
return *a > *b ? a : b; << max<int>(i,j) << “\n“
}
<< max(&i,&j) << “\n“
<< max<>(&i,&j) << “\n“
<< max<int>(&i,&j) << “\n“
<< max<int*>(&i,&j)<< “\n“
<< max(i,‘a‘) << endl;
return 0;
}
422
Überladen von Template-Funktionen (3)

template< typename T >


T max(T a, T b) {
return a > b ? a : b;
Welche Funktionen werden instanziiert und aufgerufen?
}
Welches Ergebnis wird ausgegeben?
int max(int a, int b) {
return a > b ? a : b;
int main(void){
}
int i = 5, j = 42; Ausgabe:
template< typename T > cout << max(i,j) <<42“\n“
T* max(T* a, T* b) { << max<>(i,j) <<42“\n“
return *a > *b ? a : b; << max<int>(i,j) <<42“\n“
}
<< max(&i,&j) <<0x28ccb0
“\n“ (&j)
<< max<>(&i,&j) <<0x28ccb0
“\n“ (&j)
1. Keine exakt passende Nicht-Template Funkt.
<< max<int>(&i,&j) <<0x28ccb0
“\n“ (&j)
2. Keine exakt passende Template-Funktion << max<int*>(&i,&j)<<0x28ccb4
“\n“ (&i)
3. Implizite Typumwandlung char  int << max(i,‘a‘) <<97endl;
 return 0;
Passende Nicht-Template-Funktion gefunden
}
423
Spezialisieren von Template-Funktionen (1)

 Manchmal kann Funktions-Implementierung für einen bestimmten


Datentyp sinnvoller oder effizienter implementiert werden
 Beispiel: Template-Funktion max liefert für Typ const char* nicht das
erwünschte Ergebnis! Warum!?
 Spezialisierung des Templates für einen konkreten Typ möglich
template< typename T >
T max(T a, T b) { return a > b ? a : b; }

Template-Spezialisierung für Datentyp T = const char*


template<>
const char* max<const char*>(const char* a, const char* b) {
return strcmp(a, b) > 0 ? a : b;
}

Vorsicht: Spezialisierung ≠ Überladen


Primäres Auswahlkriterium für den Compiler sind die Signaturen der generischen
Template-Funktionen. Erst wenn die passende Schablone ausgewählt wurde, sucht
der Compiler nach einer passenden Spezialisierung dieser Schablone!
424
Spezialisieren von Template-Funktionen (2)

 Welche Funktion wird ausgeführt?


template< typename T > T max(T a, T b)
{ … }
template<>
const char* max(const char* a, const char* b)
{ … }
int main(){
template<typename T> const char *s0 = "abc", *s1="aaa";
T* max(T* a, T* b) cout << max(s0,s1) << endl;
{ … }
cout << max<const char*>(s0,s1) << endl;
cout << max<const char>(s0,s1) << endl;
cout << max<>(s0,s1) << endl;
return 0;
}

Fazit: Spezialisierung und Überladen von Template-Funktionen


bieten Programmierer viel Freiheit, verlangen aber auch tiefe
Kenntnisse und hohe Verantwortung!
425
Spezialisieren von Template-Funktionen (2)

 Welche Funktion wird ausgeführt?


template< typename T > T max(T a, T b)
{ … }
template<>
const char* max(const char* a, const char* b)
{ … }
int main(){
template<typename T> const char *s0 = "abc", *s1="aaa";
T* max(T* a, T* b) cout << max(s0,s1) << endl;
{ … }
cout << max<const char*>(s0,s1) << endl;
cout << max<const char>(s0,s1) << endl;
cout << max<>(s0,s1) << endl;
return 0;
}

Fazit: Spezialisierung und Überladen von Template-Funktionen


bieten Programmierer viel Freiheit, verlangen aber auch tiefe
Kenntnisse und hohe Verantwortung!
426
Konsequenzen (1)

Eine Funktions-Schablone wird vom Compiler mehrfach


1. übersetzt. Zunächst wird die allgemeine Schablone mit dem
unbekannten Typ übersetzt.

Für jeden Funktionsaufruf im Quelltext wird die benötigte


2. Variante der Funktion zur Compile-Zeit erzeugt. Die Funktion wird
somit für jeden konkreten Typ ein zweites Mal übersetzt. Diese
nennt man Instanz. Den Vorgang bei dem sie gebildet wird, nennt
man Instanziierung oder Instanzbildung.

Funktions-Schablonen vermindern nicht den erzeugten


3. Programmcode, da eine Schablone für jeden tatsächlich
benötigten Typ expandiert wird und dabei jeweils eigenen Code
erzeugt.

427
Konsequenzen (2)

Allerdings wird der Quelltext des Programms kürzer, da die


4.
Schablone im Quelltext nur einmal angegeben werden muss.
Das ist aber genau das, was wir anstreben sollten! Wir sollten
den Quellentext kürzer und vor allem wiederverwendbar machen.
Ob dabei etwas Programmcode mehr oder weniger entsteht,
spielt eine untergeordnete Rolle - vor allem, da eine Fassung, in
der Schablonen vermieden werden, genau so viel Programmcode
erzeugen würde.

428
Konsequenzen (3)

Bevor man eine Funktion als Schablone abstrakt formuliert, sollte


5.
man eine konkrete Fassung ausgetestet haben.
Einerseits sollte man dies tun, um Syntaxfehler zu beseitigen
solange diese vom Compiler noch (mehr oder weniger) direkt an
Ort und Stelle gemeldet werden (siehe auch Punkt 1. ).
Andererseits kann man bei dieser Vorgehensweise sicher sein,
dass trotzdem auftretende Fehlermeldungen nichts mit der
Schablone selbst zu tun haben sondern z.B. mit dem Kontext des
Typs, der substituiert wird.

429
Instanziierungsproblem (1)

 Bisher:
 Trennung von Deklarationen und Header-Datei max.h
Definitionen In Header- und Quell- template< typename T >
text-Dateien T max(T a, T b);
 Getrennte Übersetzung
Quellcode-Datei max.cpp
#include “max.h“
 Bei Templates problematisch!
 Beispiel: template< typename T >
T max(T a, T b) {
 Compiler übersetzt max.cpp return a > b ? a : b;
ohne Probleme }
 Compiler übersetzt main.cpp
Programmcode main.cpp
ohne Probleme
#include “max.h“
 Linker meldet Fehlermeldung:
int main () {
„Finde keine Methoden-Instanz
return ::max(47, 11);
für Template-Funktion max<int>!“ }

430
Instanziierungsproblem (2)

 Templates werden zweimal kompiliert!


1. Übersetzung des reinen, Typ-unabhängigen Template-Codes
2. Bei Benutzung: Instanziierung und Übersetzung

 Bei Übersetzung von max.cpp:


 das Template wird kompiliert und alle Typ-unabhängigen Fehler werden
identifiziert
 Es wird keine Instanz für einen konkreten Typ angelegt!

 Bei Übersetzung von main.cpp:


 Compiler findet keine Methoden-Definition, nur Deklarationen aus Header max.h
 Kann für den Aufruf max<int>(47, 11) keine Methoden-Instanz kompilieren
 Setzt lediglich Verweise, die der Linker auflösen soll

 Linker findet keine Instanz für die gesetzten Verweise!

Compiler benötigt für Instanziierung eines Templates immer


dessen Definition!

431
Instanziierungsproblem (3)

 Lösungsansätze (State-of-the-art: Inclusion-Model)


 Template-Deklararation und Definition gemeinsam inkludieren, z.B.
durch
 Inkludieren der Implementierungsdatei im Header
 Rekursives Inkludieren durch Include-Guard verhinden
 Header-Datei max.h Quellcode-Datei max.cpp
#ifndef __MAX_H__ #ifndef __MAX_CPP__
#define __MAX_H__ #define __MAX_CPP__
#include “max.h“
template< typename T >
T max(T a, T b); template< typename T >
T max(T a, T b) {
#include “max.cpp“
return a > b ? a : b;
#endif
}
#endif Häufig wird
 Programmcode main.cpp Quelldatei dann
auch maxDef.h
#include “max.h“ benannt
int main () { return ::max(47, 11); }
432
Instanziierungsproblem (4)

 Explizites Inkludieren von Header- und Quelldatei


 Header-Datei max.h Quellcode-Datei max.cpp
#ifndef __MAX_H__ #ifndef __MAX_CPP__
#define __MAX_H__ #define __MAX_CPP__
#include “max.h“
template< typename T >
T max(T a, T b); template< typename T >
T max(T a, T b) {
#endif
return a > b ? a : b;
}
#endif
 Programmcode main.cpp Häufig wird
Quelldatei dann
#include “max.h“
auch maxDef.h
#include “max.cpp“
benannt
int main () { return ::max(47, 11); }

433
Instanziierungsproblem (5)

 Implementierung von Deklaration und Definition in einer einzigen Header-Datei


 Header-Datei max.h Programmcode main.cpp
#ifndef __MAX_H__ #include “max.h“
#define __MAX_H__ int main(){ return ::max(47, 11); }
template<typename T>
T max(T a, T b) { return a>b?a:b; }
#endif

 Alle drei Lösungswege finden in der Praxis Anwendung


 Werden von C++ unterstützt, indem z.B. identische Template-Instanzen
mehrfach im Objekt-Code mehrerer verbundener Übersetzungseinheiten
vorkommen dürfen  Objekt-Code wird unnötig aufgebläht!
 In den Implementierungsdateien dürfen keine Nicht-Template-Funktionen
oder -Klassen definiert werden!
Der C++ Sprachstandard sieht noch das Schlüsselwort export vor, um vom Compiler
eindeutige Instanzen über mehrere Übersetzungseinheiten hinweg zu verlangen,
allerdings wird dieses Schlüsselwort von KEINEM relevanten Compiler unterstützt!
434
Klassen-Templates

 Ähnlich zu Template-Funktionen können auch Klassen-Definitionen


parametrisiert werden
 Standardbeispiel: Container-Klassen, deren Element-Typ als
Parameter übergeben wird
 Eine Template Deklaration spezifiziert eine Menge von
parametrisierten Klassen (oder Funktionen).
 Ein Template wird häufig auch Schablone oder generische Klasse
genannt.
 Template wird deklariert durch:

template < [ Type-Params ] [ , [ Non-Type-Params ] ] > declaration

Type-Params None-Type-Params
 Liste von Typen bzw. Klassennamen:  Optionale Liste von zusätzlichen
class Name1, class Name2,... Parametern für die es sehr eingeschränkte
oder typename Name1, typename Name2,... Möglichkeiten gibt (siehe spätere Folien).
 durch Kommata getrennt Meistens ist es ein ganzzahliger Wert –
 Beide Varianten sind äquivalent. z.B. die maximale Größe eines Stack.
435
Klassen-Templates

 Ähnlich zu Template-Funktionen können auch Klassen-Definitionen


parametrisiert werden
 Standardbeispiel: Container-Klassen, deren Element-Typ als
Parameter übergeben wird
 Eine Template Deklaration spezifiziert eine Menge von
parametrisierten Klassen (oder Funktionen).
 Ein Template wird häufig auch Schablone oder generische Klasse
genannt.
 Template wird deklariert durch:

template < [ Type-Params ] [ , [ Non-Type-Params ] ] > declaration

Type-Params None-Type-Params
 Liste von Typen bzw. Klassennamen:  Optionale Liste von zusätzlichen
Anmerkung:
class Name1, class Name2,... Parametern für die es sehr eingeschränkte
Meist schreibt man beide Teile in der angegebenen Reihenfolge. Das ist jedoch Konvention
oder typename Name1, typename Name2,... Möglichkeiten gibt (siehe spätere Folien).
und keine Vorschrift. Beide Arten von Parametern dürfen gemischt werden.
 durch Kommata getrennt Meistens ist es ein ganzzahliger Wert –
 Beide Varianten sind äquivalent. z.B. die maximale Größe eines Stack.
436
Klassen-Templates: Listen von Daten (1)

 Wir können als Beispiel sehr einfach eine generische Listenklasse


definieren, bei der der Inhalt jeder Zelle gerade ein Datum eines
beliebigen Typs T ist.
 Die folgende Forward Definition ist nötig für die folgende friend-Definition
template <typename T> class ListenKlasse;
 Damit können wir eine Klasse für die Listenelemente selbst definieren:
template <typename T>
class ListenElement {
T inhalt;
ListenElement<T> *next;
public:
ListenElement (T t, ListenElement<T> * n) :
inhalt(t), next(n) {};
friend class ListenKlasse<T>;
};

437
Listen von Daten (2)

 Für die Bearbeitung einer kompletten Liste mit Anfangs- und Endpointer
können wir folgende Klasse deklarieren:
template <typename T>
class ListenKlasse {
ListenElement<T> *anfang, *ende;
public:
ListenKlasse () : anfang(NULL), ende(NULL) {};
~ListenKlasse();
void leeren();
void einfuegenAnfang(const T& t);
void einfuegenEnde(const T& t);
void ausgabe();
};

 Der Konstruktor erzeugt eine leere Liste, die beiden Methoden fügen ein
Listenelement jeweils am Anfang bzw. am Ende der Liste ein.
 Der Destruktor soll die gesamte Liste abarbeiten und die
einzelnen Elemente wieder löschen.
 Der Template-Parameter T der Listenklasse dient gleichzeitig
als Typ-Argument für das Template ListenElement!
438
Listen von Daten (3)

 Implementierung des Destruktors:


template <typename T>
ListenKlasse<T>::~ListenKlasse() { this->leeren(); }
template <typename T>
void ListenKlasse<T>::leeren() {
for (ListenElement<T> *zp = anfang;
anfang != NULL; zp = anfang ) {
anfang = anfang->next;
delete zp;
}
anfang = ende = NULL;
}

 zp ist jeweils der Anfang der aktuellen Liste.


 Nachdem der Anfang auf das nächste Element fortgeschrieben
wurde, kann der alte Anfang gelöscht werden.
 Destruktor und leeren() werden hier außerhalb der Klassen Deklaration
definiert, daher muss ihnen der vollständige Klassenname samt Template-
Parameter-Deklaration vorangestellt werden!
 Dies gilt für alle Nicht-Inline definierten Methoden!
439
Listen von Daten (4)

 Implementierung des Einfügens am Anfang der Liste:

template <typename T>


void ListenKlasse<T>::einfuegenAnfang(const T& t) {
ListenElement<T> *zp(new ListenElement<T>(t, NULL));
if (anfang == NULL)
anfang = ende = zp;
else
{ zp->next = anfang; anfang = zp; }
}

 Es wird ein neues Listenelement zp mit dem neuen Datum erzeugt.


 Falls die Liste noch leer ist, werden anfang und ende auf zp gesetzt.
 Andernfalls wird der Nachfolger von zp der bisherige Anfang und der
neue Anfang zp.

zp Neu D Inhalt

neu bisher

anfang
440
Listen von Daten (5)

 Implementierung des Einfügens am Ende der Liste:


template <typename T>
void ListenKlasse<T>::einfuegenEnde(const T& t) {
ListenElement<T> *zp(new ListenElement<T>(t, NULL));
if (anfang == NULL)
anfang = ende = zp;
else
{ ende->next = zp; ende = zp; }
}

 Es wird ein neues Listenelement zp mit dem neuen Datum erzeugt.


 Falls die Liste noch leer ist, werden Anfang und Ende auf zp gesetzt.
 Andernfalls wird zp der Nachfolger des bisherigen Endes und das
neue Ende.

Inhalt Neu D

bisher neu

ende zp
441
Listen von Daten (6)

 Implementierung der Ausgabe einer Liste:


template <typename T>
void ListenKlasse<T>::ausgabe() {
for(ListenElement<T> *zp(anfang); zp; zp = zp->next)
zp->inhalt.zeige();
}

 Dabei setzen wir voraus das für den Typ T bereits eine Methode zeige
implementiert ist.
 Alternativ könnten wir voraussetzen, dass für den Typ T ein überladener
Operator << definiert ist:


for(ListenElement<T> *zp(anfang); zp; zp = zp->next)
cout << zp->inhalt << endl;
}

 Der Compiler prüft bei der Instanziierung, ob der Typ T die


Anforderungen des Templates erfüllt!

442
Benutzung des Listen-Templates (1)

 Man kann mit den bisherigen Klassendefinitionen bzw. Methoden nun


Listen von Daten aufbauen. Z.B.:
ListenKlasse<FPoint> fpListe;
ListenKlasse<int> intListe;

 Konkreter Element-Typ der Liste wird als Template-Argument übergeben


 Der Compiler instanziiert und kompiliert für jeden Typ eine eigene
Listenklasse
 fpListe und intListe sind (automatische) Variablen. anfang

Der Konstruktor der Klasse erzeugt jeweils eine leere Liste. ende

Im Gegensatz zu Template-Funktionen können die Parameter bei


Template-Klassen nicht hergeleitet werden  es ist immer eine explizite
Typangabe erforderlich!

443
Benutzung des Listen-Templates (1)

 Man kann mit den bisherigen Klassendefinitionen bzw. Methoden nun


Listen von Daten aufbauen. Z.B.:
ListenKlasse<FPoint> fpListe;
ListenKlasse<int> intListe;

 Konkreter Element-Typ der Liste wird als Template-Argument übergeben


 Der Compiler instanziiert und kompiliert für jeden Typ eine eigene
Listenklasse
 fpListe und intListe sind (automatische) Variablen. anfang

Der Konstruktor der Klasse erzeugt jeweils eine leere Liste. ende

 Jetzt können wir ein erstes Element einfügen (am Anfang oder Ende):
fpListe.einfuegenAnfang(FPoint(1,1,42));
intListe.einfuegenEnde(4711);

 fpliste: intListe:
anfang (1,1,42) anfang (4711)

ende ende
444
Benutzung des Listen-Templates (2)

int main(void){
ListenKlasse<FPoint> fpListe;
fpListe.einfuegenAnfang(FPoint(2,2,42));
fpListe.einfuegenEnde(FPoint(3,3,7));
fpListe.einfuegenAnfang(FPoint(4,4,4711));
fpListe.ausgabe();

ListenKlasse<int> intListe;
intListe.einfuegenEnde(4711);
intListe.einfuegenEnde(2010);
x-Koord: 4 y-Koord: 4 Wert: 4711
intListe.einfuegenAnfang(0);
x-Koord: 2 y-Koord: 2 Wert: 42
intListe.ausgabe();
x-Koord: 3 y-Koord: 3 Wert: 7
return 0;
0
}
4711
2010

intListe: anfang (0) (4711) (2010)

fpListe: anfang (4,4,4711) (2,2,42) (3,3,7)


ende

445
ende
Non-Type-Params

 Neben Typ-Parametern können auch s.g. Non-Type-Parameter in der


Template Deklaration von Klassen- als auch Funktions-Templates
auftreten.
 Für diese gelten folgende Einschränkungen:
 Entweder: Der Typ des Parameters ist ganzzahlig oder eine Enumeration.
Der Parameter kann mit einem passenden konstanten Wert instanziiert
werden.
 Oder: Der Typ des Parameters ist ein Pointertyp. Der Parameter kann mit
einem Verweis auf ein Objekt oder eine Funktion mit globalem
Gültigkeitsbereich instanziiert werden.
 Oder: Der Typ des Parameters ist ein Referenztyp. Der Parameter kann mit
einem Verweis auf ein Objekt oder eine Funktion mit globalem
Gültigkeitsbereich instanziiert werden.
 Oder: Der Typ des Parameters ist ein Pointertyp zu einem der Typ-
Parameter. Der Parameter kann mit einem Verweis auf ein Objekt diesen
Typs instanziiert werden.

 Gleitpunktzahlen, String-Literale und Objekte dürfen


nicht als Parameter verwendet werden!
446
Non-Type-Params – Beispiele (1)

enum WDay {Mo, Tu, We, Th, Fr, Sa, Su};


template<WDay D> class A{};
template<int N> class B{};
template<typename T, T *t> class C{};
template<void f(int)> class D{};
int i = 42;
void func(int i){}
int main(void){
const int ci = 0;
A<We> a(); // enum
B<ci> b(); // int  Anstelle skalarer Werte
C<int, &i> c(); // pointer dürfen auch Ausdrücke als
D<&func> d(); // function-pointer Parameter verwendet
werden
B<42-3> e();  Alle Parameter müssen
return 0; aber zur Compile-Zeit
} bekannt sein!

447
Template-Template-Parameter (1)

 Templates dürfen selbst als Template-Parameter fungieren:


Paar< int, Paar<int, double> > a(5, Paar<int, double>(3,1.1));
Leerzeichen! Sonst Verwechslung mit >> Operator!

 In der Template-Parameterliste kann explizit gefordert werden, dass ein


übergebenes Typ-Argument selbst wieder eine Template-Klasse bezeichnet.
template <typename T, template<typename S> class C>
class CollectionWrapper{
C<T> collection; // eine generische Collection
...
};

 Der zweite Parameter template<typename S> class C der Template-


Deklaration ist selbst wieder eine Deklaration! Hier darf class nicht durch
typename ersetzt werden!
 Da der Parameter S der Template-Klasse C nicht verwendet wird, kann er
weggelassen werden.

448
Template-Template-Parameter (2)

Angenommen, es gibt drei verschiedene generische Listen-Implementierungen:


template <typename T> class ArrayListe;
template <typename T> class EinfachVerketteteListe;
template <typename T> class DoppeltVerketteteListe;
Dann werden durch die folgenden Deklarationen...
CollectionWrapper<int, ArrayListe> c0;
CollectionWrapper<char, EinfachVerketteteListe> c1;
CollectionWrapper<short, DoppeltVerketteteListe> c2;
...drei Verschiedene Instanzen von CollectionWrapper angelegt:
class CollectionWrapper{
ArrayList<int> collection;
... class CollectionWrapper{
}; EinfachVerketteteListe<char> collection;
...
class CollectionWrapper{
};
DoppeltVerketteteListe<short> collection;
...
Nicht nur
}; Datentyp, sondern auch Datenhaltung parametrisiert!
449
Default-Werte bei Argumenten

 Bei der Deklaration können analog zu einer Funktionsdeklaration


Defaultwerte vergeben werden.
template <typename T = double, int n = 1024>
class StaticStack{
T elements[n]; // Array im Stack-Speicher
...
};
 Bei der Deklaration
StaticStack <int> b;

wird als Wert des zweiten Arguments der Defaultwert benutzt. Es ist
sogar dann eine Deklaration ohne Parameter möglich:
StaticStack <> d;
// Erzeugt einen double-Stack mit 1024 Elementen.

Die spitzen Klammern sind auch bei leeren Parameterlisten


obligatorisch!

450
Instanziierung

 Bei der bisher benutzten impliziten Instanziierung von


Template-Klassen werden nur diejenigen Klassen-Methoden
kompiliert, die Im Programm tatsächlich aufgerufen werden!
 Die ListenKlasse kann so auch mit Typen instanziiert werden, für die der
Ausgabeoperator << nicht überschrieben ist, sofern die Methode
ausgabe() auf dieser Instanz nicht aufgerufen wird.

 Zusätzlich zu der impliziten Instanziierung können Template-


Klassen auch explizit erzeugt werden.
 Beispiel:
template class StaticStack<int>;
Hierbei werden insbesondere explizit Instanzen aller Methoden angelegt.

451
Beispiel: Ein generischer Stack (1)

class Stackfehler {
Eine einfache Fehlerklasse
string meldung;
public:
Stackfehler(string s){ meldung = s; }
string melde(){ return meldung; }
};

template <typename T, int max>


class StaticStack{ // Generischer Stack
int aktSize;
T stackArray[max];
public:
StaticStack() : aktSize(max) {}
int istLeer (){ return aktSize >= max;}
int istVoll (){ return aktSize <= 0;}

452
Beispiel: Ein generischer Stack (2)

void push (T e) throw (Stackfehler){


if (istVoll())
throw Stackfehler( "Fehler: Stack voll.");

stackArray[--aktSize] = e;
}

T pop () throw (Stackfehler){


if (istLeer())
throw Stackfehler( "Fehler: Zugriff auf leeren Stack.");

return stackArray[aktSize++];
}

 Wird eine Methode außerhalb der Klassendeklaration definiert, so muss vollständige


Template-Deklaration vorangestellt werden! Default-Parameter dürfen dabei nicht
noch einmal angegeben werden.

template<typename T, int n>


void StaticStack<T,n>::push (T e) throw (Stackfehler)
{ ... }
453
Anwendung des generischen Stacks (1)

 Für häufig verwendete Templates ist evtl. ein Typ-Alias sinnvoll:


typedef StaticStack<int, 512> IntStack512;
typedef StaticStack<int, 42> IntStack42;
typedef StaticStack<char> DefaultCharStack;

typedef bewirkt hier keine Instanziierung des Templates!

 Ebenso werden durch die Deklaration von Zeiger -oder Referenzvariablen


keine Instanzen erzeugt:
IntStack512 *s; // Keine Instanziierung!

Definition einiger Stacks:


int main () {
StaticStack <int, 10> s1; // Integer Stack mit 10 Elementen
StaticStack <char, 42> s2; // Char Stack mit 42 Elementen
StaticStack <double, 4711> s3; // double Stack mit 4711 Elementen
StaticStack <FPoint*, 5> s4; // FPoint* Stack mit 5 Elementen
454
Anwendung des generischen Stacks (2)

 Test des Integer-Stacks:


for (int i=0; i < 12; i++)
try {
s1.push(i);
} catch (Stackfehler sf) {
cout << sf.melde() << endl;
}

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


try {
cout << s1.pop() << " ";
} catch (Stackfehler sf) {
cout << endl << sf.melde() << endl;
}
Anfang Integer-Stack Test
Fehler: Stack voll.
Fehler: Stack voll.
9 8 7 6 5 4 3 2 1 0
Fehler: Zugriff auf leeren Stack.
Fehler: Zugriff auf leeren Stack.

455
Anwendung des generischen Stacks (3)

 Test des Char-Stacks:

s2.push('C'); s2.push('+');
s2.push('+'); s2.push(' ');
s2.push('+'); s2.push('+');
s2.push('C');
char ch;
while ( !s2.istLeer() ) {
ch = s2.pop();
cout << ch;
}
cout << endl;
Anfang Char-Stack Test
C++ ++C

456
Anwendung des generischen Stacks (4)

 Test des FPoint-Stacks:


try {
s4.push(new FPoint(1,1,7));
s4.push(new FPoint(2,2,13));
s4.push(new FPoint(3,3,42));
s4.push(new FPoint(4,4,4711));
s4.push(new FPoint(5,5,35043));
s4.push(new FPoint(6,6,35037));
} catch (Stackfehler sf) {
cout << sf.melde() << endl;
}

FPoint *pf;
while ( !s4.istLeer() ) {
Anfang FPoint-Stack Test
pf = s4.pop();
Fehler: Stack voll.
cout << (*pf) << endl;
x-Koord: 5 y-Koord: 5 Wert: 35043
}
x-Koord: 4 y-Koord: 4 Wert: 4711
x-Koord: 3 y-Koord: 3 Wert: 42
x-Koord: 2 y-Koord: 2 Wert: 13
x-Koord: 1 y-Koord: 1 Wert: 7

457
Member-Templates (1)

 Die Elemente einer Template-Klasse können selbst wieder Templates


sein.
 Beispiel: Typumwandlung durch parametrisierten Copy-Konstruktor
Die Klasse Paar besitzt einen Template-Copy-Konstruktor, mit dem die
Werte eines anderen Paares mit zuweisungskompatiblen Datentypen
kopiert werden können
(Annahme: Implizite Casts T  A bzw. U  B sind möglich)

template <typename A, typename B> class Paar {


A a; B b;
public:
Paar(const A& ax, const B& bx) : a(ax), b(bx) {}
template <typename T, typename U>
Paar(const Paar<T,U> &p) : a(p.getA()), b(p.getB()) {}

A getA() const { return a; }


B getB() const { return b; }
};
458
Member-Templates (2)

 Anwendungsbeispiel:
int main() {
Paar<short, float> x(37, 12.34);
Paar<long, long double> y(x);
cout << y.getA() << ", " << y.getB() << endl;
Paar<double, int> a(3.14, 4);
Paar<int, double> b(a); // <- Compiler-Warnung
cout << b.getA() << ", " << b.getB() << endl;
return 0;
}
Ausgabe:
37, 12.34
3, 4

459
Member-Templates (3 )

 Weitere Anmerkungen:
 Bei Definition von Member-Templates außerhalb der Klassen-Deklaration
müssen Template-Parameter kaskadierend angegeben werden.
template <typename A, typename B>
template <typename T, typename U>
Paar<A,B>::Paar(const Paar<T,U> &p)
: a(p.getA()), b(p.getB()){}

 Häufig findet man für Collections auch einen überladenen Zuweisungs-


operator, z.B.
template <typename T>
template <typename U>
Liste<T>& Liste<T>::operator=(const Liste<U>& st){...}

 Da Stack<T> und Stack<U> für T != U unterschiedliche Typen sind, (T und


U müssen nur zuweisungskompatibel sein), hat die Instanz Stack<T> nur
Zugriff auf die öffentlichen Elemente von Stack<U> (und umgekehrt).
 Evtl. friend-Deklaration notwendig!
460
Member-Templates (4 )

 Friend-Deklarationen bei ListenKlasse:


 Für den Zugriff auf die einzelnen Einträge der Liste erlauben wir
jeder Instanz von ListenKlasse den friend-Zugriff auf jede Instanz
von ListenElement.
template <typename T> class ListenElement {
...
template<typename U> friend class ListenKlasse;
};

 Analog wird jede Instanz von ListenKlasse als friend jeder anderen
Instanz von ListenKlasse deklariert.

template <typename T> class ListenKlasse {


...
template<typename U> friend class ListenKlasse;
};

461
Member-Templates (5 )

 Somit lässt sich der parametrisierte Zuweisungsoperator für


die Listenklasse implementieren durch...
template <typename T>
template <typename U>
ListenKlasse<T>& ListenKlasse<T>::operator=(
const ListenKlasse<U>& l) {
if ((void*)this == (void*)&l) // Selbstzuweisung?
return *this;

this->leeren();
ListenElement<T2> *zp(l.anfang);
while(zp != NULL){
this->einfuegenEnde(zp->inhalt);
zp = zp->next;
}

return *this;
}

462
Member-Templates (6 )

 Anwendungsbeispiel:

int main(void){
ListenKlasse<int> intListe;

ListenKlasse<float> floatListe;
floatListe.einfuegenEnde(3.14);
floatListe.einfuegenEnde(1.23);
floatListe.einfuegenAnfang(99.5);
floatListe.ausgabe();

intListe = floatListe;
intListe.ausgabe();
Ausgabe floatListe:
99.5
return 0; 3.14
} 1.23
Ausgabe intListe:
99
3
1
463
Template-Spezialisierung (1)

 Ebenso wie Template-Funktionen können auch Template-Klassen für


konkrete Typen spezialisiert werden
 Spezialisierung
 Überschreiben des Templates durch Angabe eines spezifischeren Typs
 Überschreiben ganzzahliger Parameter durch Angabe eines konkreten
Wertes
 Vollständige Template Spezialisierung
 für einen bestimmten Parametersatz, z.B. Vector<bool>
 Partielle Template Spezialisierung
 für eine Parametermenge, z.B. Vector<const T*> mit beliebigem T
template<typename A, typename B> class Paar { … }
template<> class Paar<bool, bool> {
Vollständige Spezialisierung
// Paar für Klasse bool
}
template<typename A> class Paar<A, A>{ Partielle Spezialisierung
// Paare mit identischen Datentypen
}
464
Template-Spezialisierung (2)

 Beispiel: Partielle Spezialisierung von Paar für Paare mit übereinstimmenden


Datentypen:
template<typename A, typename B> class Paar { … }

template <typename A>


class Paar<A, A> {
A t[2]; // geänderte interne Datenhaltung!

public:
Paar(const A& va, const A& vb){ t[0] = va; t[1] = vb; }

template <typename T, typename U>


Paar(const Paar<T,U> &p){ t[0]=p.getA(); t[1]=p.getB(); }

A getA() const { return t[0]; }


A getB() const { return t[1]; }
};

465
Template-Spezialisierung (3)

 Die Implementierung der Template-Spezialisierung ist unabhängig von


„Mutter“-Template
 Keine „Vererbung“ von Klassen-Membern
 Alle Member-Funktionen müssen für spezialisierten Typ neu
implementiert werden
 Verantwortung des Programmierers, Schnittstellen und Semantik des
„Mutter“-Templates zu wahren

 Findet der Compiler bei der Instanziierung sowohl eine passende


vollständige als auch eine partielle Spezialisierung, so wählt er die
vollständige
 Passt keine Spezialisierung, wird das „Mutter“-Template genommen
 Template-Spezialisierung bewirkt eine Fallunterscheidung
 Basis für Metaprogrammierung

466
Metaprogrammierung

 Darunter versteht man die Erstellung von Programmen, die andere


Programme erzeugen oder verändern.

Bei C++ bietet sich hierfür das Konzept der Templates an.

 Aus Templates wird zur Übersetzungszeit neuer Code ohne Templeates


generiert.
 Rekursive Template-Instanziierung erlaubt Fallunterscheidungen und
Schleifen zur Compile-Zeit
 Dadurch können bereits komplexe Berechnungen beim Kompilieren
durchgeführt werden.
 Tatsächlich ist man auf dieser Ebene bereits Turing-vollständig!
 Template-Programmierung ist funktional: keine Mehrfachzuweisung
von Variablen

 Hier ist das erste C++-Metaprogramm:


http://www.erwin-unruh.de/Prim.html

467
Beispiel

 Durch Templates lassen sich die bekannten


Kontrollstrukturen umsetzen:

template<bool C> class ifstat{ };

class ifstat<true> {
public: static inline void f() { stat1; }
};

class ifstat<false> {
public: static inline void f() { stat2; }
};

// Replacement for 'if/else' statement:


ifstat<condition>::f();

468
Beispiel

template<int N> class Fibonacci {


public:
enum {
result = Fibonacci<N-1>::result +
Fibonacci<N-2>::result Rekursiver
}; Aufruf des
};
Templates
template<> class Fibonacci<2> {
public:
enum { result = 1 };
};

template<> class Fibonacci<1> {


public:
enum { result = 1 };
};

469
Vor- und Nachteile

 Abwägung zwischen Übersetzungszeit und Ausführungszeit:


 Lange Übersetzungszeit vs. kurze Laufzeiten
 Durch Templates kann der Benutzercode kürzer werden.
 Höherer Grad der Abstraktion
 Dies kann auch dazu führen, dass Fehler vermieden und somit auch
Wartungsaufwand reduziert werden.
 Programmcode wird automatisch generiert.
 Programme werden durch Template-Programmierung nicht
nur in C++ unleserlich.
 Dies kann die Wartbarkeit von Programmen teurer
machen.
 Portierbarkeit ist eingeschränkt, da Unterschiede bei den
Compilern existieren.

470
STL
Standard Template Library

Einführung
Container
Vector
Iteratoren
Vector, Deque, List
Assoziative Container
Funktionsobjekte
Algorithmen
Einführung

 STL ist eine standardisierte Bibliothek von Algorithmen und


Datenstrukturen, die ursprünglich bei HP entwickelt wurde.
 Die Bibliothek bietet eine Vielzahl generischer Strukturen, die alle mittels
Templates implementiert wurden.
 Aus Gründen der Effizienz wurde auf polymorphe Klassenhierarchien und
virtuelle Methoden ganz verzichtet.

 Wesentliche Bestandteile von STL sind


Container (Behälter)
 Verwalten Mengen bzw. Multimengen von Objekten.
Iteratoren
 Dienen zum Zugriff auf jedes der Objekte eines
Containers (unabhängig vom konkreten Container).
Algorithmen
 zur Verarbeitung der Objekte eines Container.

472
Container (1)

 Container (Behälter) sind Datenstrukturen, die Mengen bzw.


Multimengen von Objekten verwalten.
 Um eine dieser Strukturen zu nutzen, muss ein entsprechender
include-Befehl verwendet werden.
 Es gibt sequentielle und assoziative Container-Klassen.
 Bei sequentiellen Containern ist der Zugriff auf die Elemente nur der
Reihe nach möglich. Beispiele:
 list: Eine doppelt verkettete, lineare Liste. Schnelles einfügen und löschen an
jeder beliebigen Position. Kein direkter Zugriff auf die Elemente.
 deque: Eine doppelseitige Warteschlange. Schnelles einfügen und löschen
am Anfang und am Ende. Direkter Zugriff auf jedes Element.
 vector: Ein dynamisches Array. Schnelles einfügen und löschen am Ende.
Direkter Zugriff auf jedes Element.

473
Container (2)

 Bei assoziativen Containern ist der Zugriff auf die Elemente über
Schlüssel möglich. Der Aufwand für einen Zugriff mit einem Schlüssel
ist logarithmisch (Bäume) oder im Durchschnitt konstant (Hashing).
 Beispiele:
 set: Eine Menge von (eindeutigen) Elementen.
 multiset: Eine „Menge“, in der Elemente nicht eindeutig sein müssen.
 map: Eine Struktur zur Verwaltung von Paaren (Schlüssel,Wert), wobei es zu
jedem Schlüssel höchstens einen Wert gibt.
 multimap: Eine Struktur zur Verwaltung von Paaren (Schlüssel,Wert), wobei es
zu jedem Schlüssel mehrere Werte geben kann.

474
Container (3)

 Außer den sequentiellen und assoziativen Container-Klassen gibt


es noch Container-Adapter. Diese spezialisieren bereits definierte
Container für Spezialaufgaben, bzw. mit einer besonderen
Schnittstelle:
 stack: Spezielle Schnittstelle: push, pop und top. Ein Stack spezialisiert
einen vector, eine deque oder eine list.
 queue: Eine spezielle deque.
 priority_queue: dito.

475
Vektoren

 Ein Vektor hat eine aktuelle Größe und eine Kapazität. Diese ist ggf. größer
und beinhaltet dann „Reservezellen“, die bereits allokiert sind, aber noch
nicht gebraucht wurden. Ein Vektor der Größe 6 und Kapazität 7:

 Wichtige Methoden für Vektoren sind:


front() back()
push_back(e)

pop_back()
at()
oder
[]
476
Einführendes Beispiel: Vector (1)
 Wir definieren drei dynamische Arrays mit Hilfe der vordefinierten STL-
Klasse vector:
#include <iostream>
#include <vector>
using namespace std;

int main(){
vector<int> v1;
vector<int> v2(5);
vector<int> v3(9,3);
}

 Der erste Konstruktor definiert einen leeren Behälter für int-Werte.


 Der zweite Konstruktor, definiert einen Behälter mit 5 int-Werten;
 Inhalt: 0 0 0 0 0.
 Der dritte Konstruktor, definiert einen Behälter mit 9 int-Werten;
 Inhalt: 3 3 3 3 3 3 3 3 3 .
477
Einführendes Beispiel: Vector (2)
 Vektoren haben eine aktuelle Größe und Kapazität sowie eine maximale
Größe:
cout << "Size(v1) am Anfang: " << v1.size() << " Kapazität: "
<< v1.capacity() << " max-size: " << v1.max_size() << endl;

cout << "Size(v2) am Anfang: " << v2.size() << " Kapazität: "
<< v2.capacity() << " max-size: " << v2.max_size() << endl;

cout << "Size(v3) am Anfang: " << v3.size() << " Kapazität: "
<< v3.capacity() << " max-size: " << v3.max_size() << endl;

Size(v1) am Anfang: 0 Kapazität: 0 max-size: 1073741823


Size(v2) am Anfang: 5 Kapazität: 5 max-size: 1073741823
Size(v3) am Anfang: 9 Kapazität: 9 max-size: 1073741823

478
Einführendes Beispiel: Vector (3)
 Wir können die Vektoren füllen und danach Inhalt und Größe abfragen:
for (int i = 0; i < 10; i++) { v1.push_back(10+i);
v2.push_back(20+i); v3.push_back(30+i);}
cout << "Size(v1) nach erster Belegung: " << v1.size()
<< " Kapazität: " << v1.capacity() << endl;
// dito für v2 und v3
cout << "Inhalt von v1:" << endl;
for (int i = 0; i< v1.size(); i++)
cout << v1[i] << " "; cout << endl;
// dito für v2 und v3

Size(v1) nach erster Belegung: 10 Kapazität: 13


Size(v2) nach erster Belegung: 15 Kapazität: 15
Size(v3) nach erster Belegung: 19 Kapazität: 19
Inhalt von v1:
10 11 12 13 14 15 16 17 18 19
Inhalt von v2:
0 0 0 0 0 20 21 22 23 24 25 26 27 28 29
Inhalt von v3:
479
3 3 3 3 3 3 3 3 3 30 31 32 33 34 35 36 37 38 39
Einführendes Beispiel: Vector (4)
 Mit resize kann die Größe der Vektoren geändert werden:
v1.resize(20);
v2.resize(25, 42);
v3.resize(3, 7); // selber Effekt wie: v3.resize(3);
cout << "Size(v1) jetzt: " << v1.size()
<< " Inhalt von v1:" << endl;
// usw…

Size(v1) jetzt: 20 Kapazität: 20 Inhalt von v1:


10 11 12 13 14 15 16 17 18 19 0 0 0 0 0 0 0 0 0 0
Size(v2) jetzt: 25 Kapazität: 25 Inhalt von v2:
0 0 0 0 0 20 21 22 23 24 25 26 27 28 29 42 42 42 42
42 42 42 42 42 42
Size(v3) jetzt: 3 Kapazität: 19 Inhalt von v3:
3 3 3

 Bei der Reduktion der Größe mit v3.resize(3,7) spielt das zweite
Argument natürlich keine Rolle!
480
Einführendes Beispiel: Vector (5)

 Zugriff auf die Elemente mit eckigen Klammern oder mit der Methode at:

v3[1] = 42;
v3.at(2) = 43;

cout << "Size(v3) nach Zugriff: " << v3.size()


<< " Inhalt von v3:" << endl;
// usw…

Size(v3) nach Zugriff: 3 Inhalt von v3:


3 42 43

481
Iteratoren Motivation

 Für ein einfaches Array ist folgender Zugriff typisch:


const int max = 7;
int a[max] = { 0, 1, 2, 3, 4, 5, 6 };
cout << "Inhalt von a:" << endl;
for (int * ptr = a; ptr != a + max; ptr++)
cout << *ptr << " "; Inhalt von a:
cout << endl; 0 1 2 3 4 5 6

 Mit einem Iterator kann man ein ähnliches Verhalten für Container erhalten.
Damit ergibt sich eine für Container typische Bearbeitungsform:

cout << "Inhalt von v1:" << endl;


for (vector<int>::iterator iter1 = v1.begin();
iter1 != v1.end(); iter1++)
cout << *iter1 << " ";
cout << endl;
Inhalt von v1:
10 11 12 13 14 15 16 17 18 19
482
Weitere Iteratoren
 Der Zugriff auf die Elemente des Containers erfolgt ausschließlich lesend.
Wir hätten daher auch einen Iterator mit konstantem Ergebnispointer
benutzen können (und sollen):
cout << "Inhalt von v1:" << endl;
for (vector<int>::const_iterator iter1 = v1.begin();
iter1 != v1.end(); iter1++)
cout << *iter1 << " ";
cout << endl;

 Mit einem Reverse_Iterator können wir die Ausgabe in umgekehrter


Reihenfolge erhalten (bzw. const_reverse_iterator bei lesendem Zugriff):
cout << "Inhalt (reverse) von v1:" << endl;
for (vector<int>::reverse_iterator iter1 = v1.rbegin();
iter1 != v1.rend(); iter1++)
cout << *iter1 << " ";
cout << endl;
Inhalt (reverse) von v1:
483
19 18 17 16 15 14 13 12 11 10
Nutzung von Iteratoren (1)

 Ein Iterator verhält sich wie ein Pointer auf die Elemente eines Containers!
 Es ist daher auch schreibender Zugriff möglich:

vector<int>::iterator p;
for ( p = v1.begin(); p != v1.end(); p++) *p += 42;
cout << "Inhalt von v1 jetzt:" << endl;
for (vector<int>::const_iterator iter1 ………… // usw.

Inhalt von v1 jetzt:


52 53 54 55 56 57 58 59 60 61

 Der verwendete Iterator p darf in diesem Fall natürlich kein const-iterator


sein!

484
Nutzung von Iteratoren (2)
 Nochmal: Ein Iterator verhält sich wie ein Pointer auf die Elemente eines
Containers!
 Zugriff auf das vierte Element eines Vektors:
vector<int>::iterator p = v1.begin();
*(p+3) = 4711;
Inhalt von v1 jetzt:
 Eine Indexüberprüfung findet 10 11 12 4711 14 15 16 17 18 19
anscheinend nicht statt.
*(p+33) = 4711; hat manchmal keinen sichtbaren Effekt.

 Achtung: end() liefert einen Pointer hinter das letzte Element eines
Behälters. Die folgende Sequenz enthält also einen verbotenen
Pointerzugriff: vector<int>::iterator q = v1.end();
*q = 4711;

 „Richtig“ wäre: vector<int>::iterator q = v1.end()-1;


*q = 4711;
485
Nutzung von Iteratoren (3)
 Wenn v ein Vektor ist, dann liefern v.begin und v.end Iteratoren auf Anfang
und hinter das Ende des Vektors und v.rbegin bzw. v.rend auf Anfang und
Ende in umgekehrter Richtung.
 v.insert nimmt einen Iterator p als Argument und fügt ein Element vor der
Stelle ein, auf die p zeigt.
 Das Ergebnis ist ein Iterator, der auf das eingefügte Element zeigt. Es
gibt auch eine weitere Form von insert bei der die Anzahl der
Einfügungen angegeben werden kann. (Ergebnis ist void).
Inhalt von v1 jetzt(1):
10 11 12 4711 14 15 16 17 18 19

vector<int>::iterator q = v1.insert(p+3, 4710);


q = v1.insert(q,4709);
v1.insert(q, 3, 4700);

Inhalt von v1 jetzt(2):


10 11 12 4700 4700 4700 4709 4710 4711 14 15 16 17 18 19
486
Nutzung von Iteratoren (4)
p = v1.end();
v1.insert(p, 99);

Inhalt von v1 jetzt(3):


10 11 12 4700 4700 4700 4709 4710 4711 14 15 16 17 18 19 99

 Auf diese Weise wurde ein Element am Ende des Containers eingefügt!
p = v1.end();
 Zugriffe der Form
v1.insert(p+1, 99);
führen manchmal zum Abbruch des Programms („wollen Sie einen
Problembericht an Microsoft schicken?“) manchmal aber auch nur zu
unerwarteten Ergebnissen.
 Das Problem sollte theoretisch verschwinden, wenn man versucht eine
Ausnahme abzufangen, tut es aber nicht :
try {
p = v1.end();
v1.insert(p+1, 99);
} catch (out_of_range e){cout << "\nException: " << e.what();}
487
Nutzung von Iteratoren (5)
 v.erase(p) löscht das Element auf das der Iterator p zeigt.
 v.erase(p, q) löscht alle Elemente von p bis zu dem Element
unmittelbar vor dem Iterator q. Das Ergebnis ist ein Iterator auf das erste
Element nach den Gelöschten.
 v.clear() löscht alle Elemente.
 v1.swap(v2) vertauscht den Inhalt zweier Behälter.
 v.empty() ist true, wenn der Behälter leer ist – also z.B. nach
v1.erase(v1.begin(), v1.end());
cout << v1.empty();

oder nach v1.clear();


cout << v1.empty();

 Löschen aller Elemente, bis auf das erste und letzte:


v1.erase(v1.begin()+1, v1.end()-1);

 Vertauschen der Inhalte zweier Container:  v1.swap(v2);


488
Nutzung von Iteratoren (6)
 Die Ausgabe von Vektoren hätten wir natürlich vereinfachen können, wenn
ein Operator << für Vektoren definiert wäre.
 Diesen können wir allerdings selbst definieren:
template<typename T>
ostream& operator<<(ostream& os, const vector<T>& v){
for (typename vector<T>::const_iterator i = v.begin();
i != v.end(); i++)
os << *i << ' ';
os << endl;
return os;
}

 Nutzung:

cout << "Inhalt von v1:" << endl << v1;


cout << "Inhalt von v2:" << endl << v2;
cout << "Inhalt von v3:" << endl << v3;

489
Nutzung von Iteratoren (7)

 Für Container sind Copy-Konstruktoren und der Zuweisungsoperator


definiert.
vector<int> v1, v2, v3;
for (int i = 0; i < 10; i++) { v1.push_back(10+i); };
v2 = v3 = v1;
v3.insert(v3.end(),42);

 Für Container sind die Operatoren == und < definiert. Indirekt sind dadurch
auch !=, >, <= und >= definiert.
 Gleichheit gilt, wenn die Anzahl der Elemente gleich ist und bei einer
Iteration über beide Container elementweise Gleichheit gilt.
 < beruht auf einem ähnlichen Vergleich und basiert auf der
lexikographischen Ordnung.
 Es gilt also: v1 == v2
v1 != v3
v1 < v3
v1 <= v2
490
Nutzung von Iteratoren (8)
 Möchte man die Objekte einer eigenen Klasse mit einem vector verwalten,
müssen die unterstützten Operationen bereitgestellt werden! Mindestens die
Folgenden (das reicht aber meist nicht):
 Ein Default-Konstruktor class DoV {
 Operator < double x;
 Operator ==
public:
DoV(double d = 3.14): x(d) {}
und optional z.B. double getX() {return x;}
 Operator << };
bool operator<(DoV x, DoV y){
return x.getX() < y.getX();
}
bool operator==(DoV x, DoV y) {
return x.getX() == y.getX();
}
ostream& operator <<(ostream& os, DoV d){
os << d.getX();
return os;
}
int main(){
vector<DoV> v1, v2, v3;
...
491
STL Header Dateien

 <vector>
 <list>
 <deque>
 <queue>
Enthält queue und priority_queue
 <stack>
 <map>
Enthält map und multimap
 <set>
Enthält set und multiset
 <bitset>

492
STL Pseudo Datentypen

 value_type: Der Typ T der Elemente eines Containers


 reference: Referenz auf value_type also i.A. T&
 const_reference: dito aber const
 pointer: Pointer auf value_type also i.A. T*
 iterator und reverse_iterator: Iteratoren
 const_iterator und const_reverse_iterator : dito aber const
 difference_type: Typ der Differenz zweier Elemente: „Abstand“
 size_type: Typ der Größe eines Elements: „Index“

493
STL Elementfunktionen für Container:
Mindestausstattung

 Default constructor, Copy constructor und Destructor


 empty: Testet ob der Container leer ist.
 max_size: Maximale Größe eines Containers.
 size: Aktuelle Größe eines Containers.
 capacity: Aktuelle allokierte Größe eines Containers..
 Operatoren = == != < <= > >=
 swap: Vertauscht den Inhalt zweier Container.
 begin und end: Iteratoren auf den Anfang bzw. hinter das Ende.
 rbegin und rend: dito aber im Rückwärtsgang.
 insert: Einfügen von Elementen in einen Container.
 erase: Löschen von Elementen aus einen Container.
 clear: Löschen aller Elementen aus einem Container.

494
Typen von Iteratoren

Input - Iteratoren Output - Iteratoren

Forward - Iteratoren

Bidirectional - Iteratoren

Random Access - Iteratoren


495
Typen von Iteratoren

 Random Access
Schreiben und Lesen von Werten
Zugriff auf eine beliebige Position mit dem Iterator, z.B. p+5
 Bidirectional
Schreiben und Lesen von Werten
Sequentiell vorwärts und rückwärts bewegen auf dem Iterator
 Forward
Schreiben und Lesen von Werten
Sequentiell vorwärts bewegen auf dem Iterator
 Input
Lesen von Werten
Sequentiell vorwärts bewegen
 Output
Schreiben von Werten
Sequentiell rückwärts bewegen

496
Iteratoren und Container

 Container liefern mit dem Aufruf von begin() und end() Iteratoren
zurück.

 Der Typ des Iterators hängt dabei von der jeweiligen


Containerklasse ab.
 Effizienz bei der Implementierung der Operationen

 Input/Ouput Iteratoren werden oft bei der Ein- und Ausgabe von
Daten verwendet und haben somit einige Besonderheiten.
 Siehe Literatur

497
Operationen auf allen Iteratoren

 Operatoren für Iteratoren p und q


 Zuweisung p = q
 Inkrementierung (Präfix, Postfix) ++p und p++

498
Input und Output Iteratoren

 Zusätzliche Operationen für Input Iteratoren


 Gleichheit/Ungleichheit p == q und p != q
 Dereferenzierung *p aber nur als R-Wert (rechts der Zuweisung)
 Zusätzliche Operationen für Output Iteratoren
 Dereferenzierung *p aber nur als L-Wert (links der Zuweisung)
 Beispiel:

template<typename InputIterator, typename OutputIterator>


OutputIterator copy(InputIterator cur, InputIterator last,
OutputIterator result){
while (cur != last) *result++ = *cur++;
return result;
}

499
Forward Iteratoren

 Zusätzliche Operatoren für Forward-Iteratoren:


 Alle Operationen von Input- und Output-Iteratoren
 Also insbesondere Dereferenzierung *p als L- und R-Wert.
 Beispiel:

template<typename ForIter, typename T>

ForIter findLinear(ForIter cur, ForIter last, const T&


value) {
for ( ;cur != last; cur++)
if (*cur == value) return cur;
return last;
}

500
Bidirektionale Iteratoren

 Zusätzlich zu den Operatoren des Forward Iterator


 Dekrementierung (Präfix, Postfix) --
 Beispiel:

template<typename BidiIterator, typename T>


void reverse(BidiIterator first, BidiIterator last) {
while ( first != last && first != --last){
T tmp = *first;
*first = *last;
*last = tmp;
++first;
}
}

501
Random Access Iteratoren

 Zusätzliche Operatoren für Iteratoren p, q und ein int i:


 p += i inkrementiert p um i Positionen
 p -= i dekrementiert p um i Positionen
 p + i ergibt eine um i inkrementierte Position
 p - i ergibt eine um i dekrementierte Position
 p[i] Zugriff auf die i.te Position nach p
 p – q Distanz zwischen p und q
 Relationale Operatoren: <, <=, >, >=

 Dieser Typ von Iterator wird z. B. durch einen Vector zur


Verfügung gestellt.

502
Beispiel mit Random Access Iterator

 Mittels Iteratoren kann man vom zugrundeliegenden Container


abstrahieren.
 Teilweise sind Pointer und Iteratoren austauschbar.
 Beispiel (binäre Suche mit Iteratoren)

template<typename T, typename RandIter>

RandIter bSearch (RandIter first, RandIter last, const T& value){


RandIter notFound = last;
while (first != last) {
RandIter mid = first + (last - first)/2;
if(value == *mid) return mid;
if(value < *mid) last = mid;
else first = mid+1;
}
return notFound;
}

503
Aufruf von bSearch (1)
 Zunächst mit einem Array (OHNE STL):
int main(){
int x[] = {0,1,2,3,4,5,6,7,8,9};
cout << "Array x : ";
for (int i = 0; i < 10; i++) cout << x[i] << ' ';
cout << endl;

int val = 42;


cout << val;
int * found = bSearch(&x[0], &x[10], val);
if (*found != val) cout << " nicht";
cout << " gefunden !" << endl;

val = 7;
cout << val;
found = bSearch(&x[0], &x[10], val);
if (*found != val) cout << " nicht";
cout << " gefunden !" << endl;
Array x : 0 1 2 3 4 5 6 7 8 9

42 nicht gefunden !
504
7 gefunden !
Aufruf von bSearch (2)
 und dann mit einem Vector:
vector<int> v;
for (int i = 0; i < 10; i++) v.push_back(10+i);
cout << "Vektor v : " << v;

val = 42;
cout << val;
vector<int>::iterator it;
it = bSearch(v.begin(), v.end(), val);
if (*it != val) cout << " nicht";
cout << " gefunden !" << endl;

val = 17;
cout << val;
it = bSearch(v.begin(), v.end(), val);
if (*it != val) cout << " nicht";
cout << " gefunden !" << endl;
}
Vektor v : 10 11 12 13 14 15 16 17 18 19
42 nicht gefunden !
505
17 gefunden !
Zusammenfassung (Iteratoren)

Member Function Input Output Forward Bidirectional Random Access


Iter x(y) J J J J J
Iter x = y J J J J J
x == y J N J J J
x != y J N J J J
x++, ++x J J J J J
x--, --x N N N J J
*x rvalue lvalue J J J
(*x).f J N J J J
x->f J N J J J
x+n N N N N J
x += n N N N N J
x-n N N N N J
X -= n N N N N J
X[n] N N N N J

506
Nochmal: Vektoren
empty()
 Wichtige Methoden für Vektoren sind:
clear()
size()
front() back()
capacity()
push_back(e)
resize()
reserve()
assign()
pop_back() swap()
at()
oder
[] begin()
end()
 Wichtige Iteratoren für Vektoren sind: rbegin()
rend()
insert()
erase()
Das Einsetzen von Elementen am Ende
ist effizient – das Einsetzen und Löschen
von Elementen in der Mitte aufwändig!

507
Deque
Methoden und
 Double ended Queue. Iteratoren ähnlich
 Ähnlich wie ein Vector, zusätzlich: wie bei Vector:
Zugriff am Anfang. empty()
front() back() clear()
push_front(e)
push_back(e) size()
capacity()
resize()
pop_front() pop_back() reserve()
assign()
at() swap()
oder
[]

begin()
Das Einsetzen von Elementen am Anfang und am Ende end()
ist effizient – das Einsetzen und Löschen rbegin()
von Elementen in der Mitte aufwändig! rend()
insert()
erase()
508
List

 Kein direkter Zugriff mit at() oder [] empty()


 Die Iteratoren sind nicht Random Access sondern bidirektional! clear()
size()
assign()
front() back() swap()
push_front(e)
push_back(e)
begin()
end()
rbegin()
pop_front() pop_back() rend()
insert()
Das Einsetzen und Löschen erase()
von Elementen ist überall effizient möglich!
splice()
… remove()
unique()
merge()
reverse()
sort()
Pointer auf Anfang und Ende
509
Reverse

 Die Methode reverse() kehrt die Reihenfolge der


Listenelemente um.
 Naheliegenderweise geschieht das durch Ändern der Pointer
und ist daher effizienter als Methoden, die die Elemente ggf.
vertauschen.

510
Splice (1)

 Splice ist für zwei verschiedene Listen x und y definiert.


Aufrufform 1:
void splice(iterator position, list& x);

 Die Elemente der Liste x werden in die Liste y vor position eingefügt

x:

y:
position

 Die Liste x ist anschließend leer.

511
Splice (2)

Aufrufform 2:
void splice(iterator position, list& x, iterator i);

 Das Element, auf das i zeigt, wird in x gelöscht und in y vor position
eingefügt

x:

y:
position

512
Splice (3)

Aufrufform 3:
void splice(iterator position, list& x,
iterator first, iterator last);

 Die Elemente, zwischen first und last werden aus x gelöscht und in y vor
position eingefügt

first last

x:

y:
position

513
Remove und Unique

void remove(const T& value);


template<class Predicate> void remove_if(Predicate pred);

 Diese beiden Funktionen entfernen alle Elemente einer Liste für die mit
einem geeigneten Listeniterator i gilt *i == value,
bzw. für die gilt: pred(*i) ist true
void unique();
template<class BinaryPredicate>
void unique(BinaryPredicate pred);

 Diese beiden Funktionen entfernen alle Elemente einer Liste für die mit
einem geeigneten Listeniterator i gilt *i == *(i-1),
bzw. für die gilt: pred(*i, *(i-1)) ist true
 Das Entfernen erfolgt ggf. mehrfach, so das bei einer sortierten Liste nach
dem Aufruf von unique jedes Element nur noch einmal vorkommt

514
Sort und Merge

void sort();
template<class Compare> void sort(Compare comp);

 Da für Listen keine Random Access Iteratoren definiert sind, können die
(noch zu besprechenden) Algorithmen zum Sortieren nicht angewendet
werden.
 Daher gibt es eine eigene Methode zum Sortieren einer Liste. Diese setzt
einen Operator < für die Elemente voraus.

void merge(list<T>& x);


template<class Compare>
void merge(list<T>& x, Compare comp);

 Merge setzt zwei sortierte Listen x und y voraus. Die Elemente der Liste x
werden in die aufrufende Liste y einsortiert. x ist anschliessend leer.

515
Listen Beispiel (1)

 Das folgende Beispiel ist aus dem Buch von Kuhlins und Schrader
 Die C++-Standardbibliothek. Einführung und Nachschlagewerk, Springer
2005.

#include <algorithm>
#include <functional>
#include <iostream>
#include <list>
using namespace std;

template<class T>
ostream& operator<<(ostream& os, const list<T>& l) {
if (l.empty())return os << "leer";
for (typename list<T>::const_iterator i = l.begin();
i != l.end(); ++i)
os << *i << " ";
return os;
}

516
Listen Beispiel (2)

 Zwei Listen werden angelegt:

int main() {
list<int> a, b;
for (int i = 1; i < 9; ++i)
if (i % 2 == 1)a.push_front(i);
else b.push_back(i);
cout << "a = " << a << "und b = " << b << endl;

a = 7 5 3 1

und
b = 2 4 6 8

517
Listen Beispiel (3)

 Die Liste a wird sortiert.


 Eine neue Liste c wird mit a initialisiert.
 Die Liste a wird dann mit dieser Kopie von a gemischt.


a.sort();
cout << "a = " << a << endl;
list<int> c(a);
a.merge(c); // a mit einer Kopie von a mischen
cout << "a = " << a << endl;int main() {

a = 1 3 5 7
a = 1 1 3 3 5 5 7 7

518
Listen Beispiel (4)

 Das erste Element von a wird mit splice ans Ende verschoben.
 Die Liste b wird mit reverse umgedreht.
 In b wird das Element 4 gesucht. Mit splice werden alle Elemente von b ab
der Position von 4 an den Anfang von a verschoben.


a.splice(a.end(), a, a.begin());
cout << "a = " << a << endl;
b.reverse();
cout << "b = " << b << endl;
list<int>::iterator b4 = find(b.begin(), b.end(), 4);
a.splice(a.begin(), b, b4, b.end());
cout << "a = " << a << "und b = " << b << endl; …

a = 1 3 3 5 5 7 7 1
b = 8 6 4 2
a = 4 2 1 3 3 5 5 7 7 1 und b = 8 6

519
Listen Beispiel (5)

 Die restlichen Elemente von b werden mit splice ans Ende von a
verschoben.
 Alle Elemente 5 werden mit remove aus a entfernt.
 Mit unique werden benachbarte doppelte Elemente entfernt.


a.splice(a.end(), b);
cout << "a = " << a << "und b = " << b << endl;
a.remove(5);
cout << "a = " << a << endl;
a.unique();
cout << "a = " << a << endl;
}

a = 4 2 1 3 3 5 5 7 7 1 8 6 und b = leer
a = 4 2 1 3 3 7 7 1 8 6
a = 4 2 1 3 7 1 8 6
520
Listen Beispiel (6)

 Wenn man vor dem Aufruf von unique nochmals sortiert, fliegen
alle doppelten heraus:


cout << "a = " << a << "und b = " << b << endl;
a.sort();
cout << "a = " << a << endl;
a.unique();
cout << "a = " << a << endl;
}

...
a = 4 2 1 3 3 5 5 7 7 1 8 6 und b = leer
a = 1 1 2 3 3 4 5 5 6 7 7 8
a = 1 2 3 4 5 6 7 8

521
Aufwand

vector deque list


Einfügen und Entfernen
Vorne O(n) O(1) O(1)
Hinten O(1) O(1) O(1)
Mitte O(n) O(n) O(1)
Zugriff
erstes O(1) O(1) O(1)
letztes O(1) O(1) O(1)
mittleres O(1) O(n) O(n)
Iterator Random A Random A Bidirect
522
Funktionsobjekte (1)

 Für die nun folgenden assoziativen Container benötigen wir


Funktionsobjekte, um Vergleichsfunktionen als Parameter übergeben
zu können:
 Ein Funktionsobjekt stammt aus einer Klasse, in der der Operator ()
überschrieben ist.
 Beispiel:
template <class T>
struct MyLess<T,T> { // Binäre Funktionsklasse
bool operator() (const T& x, const T& y) {
return x < y;
}
};
int main(){
MyLess<int> bbb; // Deklaration eines Funktionsobjekts
if (bbb(2,3)) // Aufruf des Funktionsobjekts
cout << "kleiner";
cout << endl;
}
523
Funktionsobjekte (2)

 In STL gibt es spezielle Oberklassen für Funktionsklassen


 Binäre Funktionen
template<class Arg1, class Arg2, class Result>
struct binary_function {
typedef Arg1 first_argument_type;
typedef Arg2 second_argument_type;
typedef Result result_type;
};
Damit lässt sich MyLess folgendermaßen implementieren:
template <class T>
struct MyLess: binary_function<const T&, const T&, bool>
{
result_type operator()
(first_argument_type x, second_argument_type y) {
return x < y;
}
};
 Unäre Funktionen

524
Assoziative Container (1)

 Bei assoziativen Containern ist der Zugriff auf die Elemente über
Schlüssel möglich. Der Aufwand für einen Zugriff mit einem Schlüssel
ist meistens logarithmisch. Beispiele:
 set: Eine Menge von (eindeutigen) Elementen.
 multiset: Eine „Menge“, in der Elemente nicht eindeutig sein müssen.
 map: Eine Struktur zur Verwaltung von Paaren (Schlüssel, Wert), wobei es zu
jedem Schlüssel höchstens einen Wert gibt.
 multimap: Eine Struktur zur Verwaltung von Paaren (Schlüssel, Wert), wobei es
zu jedem Schlüssel mehrere Werte geben kann.

Schlüssel eindeutig mehrdeutig


Nur Schlüssel set multiset
Paare (Schlüssel,Wert) map multimap
525
Assoziative Container (2)

 Jede der vier assoziativen Container-Typen besitzt einen


Template-Parameter für den Schlüssel: Key
 Map und Multimap benötigen zusätzlich einen Template-Parameter für den
Typ T der Werte. Der Elementtyp ist:
Pair<const Key, T>
 Benötigt wird für alle vier Klassen ein weiterer Parameter: Compare.
 Compare ist der Typ für ein Funktionsobjekt, das zum Vergleichen der
Schlüssel benutzt wird. Standardmäßig wird < verwendet. Andere Objekte
für Compare sollten sich so verhalten wie <
 Assoziative Container verfügen über bidirektionale Iteratoren.

template<class Key, class T, class Compare = less<Key>,


class Allocator = allocator<pair<const Key, T> > >
class map { … }

Dito für multimap, set und multiset

526
Beispiel für die Anwendung eines Map-
Containers (1)

 Wir wollen eine Tabelle verwalten, in der Namen (vom Typ string)
und Geburtsjahre (vom Typ int) als Paare von Schlüssel und Wert
verwaltet werden sollen.

#include <iostream>
#include <string>
#include <map>
using namespace std;

typedef map<string, int> GebJahrTabelle;


typedef GebJahrTabelle::value_type VT;

int main(){
GebJahrTabelle gt;

}

527
Beispiel für die Anwendung eines Map Containers (2)

 Wir können Werte in die Tabelle eingeben und dabei den Operator []
benutzen. Dieser ermöglicht es uns über den Schlüssel auf (ggf. neue)
Elemente zuzugreifen. Der Operator [] ist nur für assoziative Container vom
Typ map definiert.
 Mit [] werden Elemente mit dem Schlüssel wie mit einem Index
angesprochen: …
gt["Arwen Undomil"] = 241;
gt["Bilbo Baggins"] = 2890;
gt["Frodo Baggins"] = 2968;
gt["Boromir"] = 2978;
gt["Aragorn"] = 2931;

 Alternativ gibt es drei Varianten dazu mit „konventionellem“ Einsetzen:

gt.insert( VT("Pippin Took", 2990));


gt.insert( pair<const string, int>("Merry Brandybuck", 2982));
gt.insert( make_pair(string("Sam Gamgee"), int(2980)));

528
Beispiel für die Anwendung eines Map-Containers (3)

 Nun können wir uns die Tabelle mit einem Iterator anschauen:


cout << "Tabelle der Geburtsjahre: " << endl;
GebJahrTabelle::const_iterator i = gt.begin();
for (;i != gt.end(); ++i)
cout << " (" << i-> first << ": " << i->second << ")\n";
// oder (*i).first bzw. (*i).second

 Der Iterator i zeigt jetzt auf eine Paar-Struktur, deren beide Elemente mit
i->first und i->second angesprochen werden können.
 Die Ausgabe erfogt nicht Tabelle der Geburtsjahre:
in der Reihenfolge der Eingabe, (Aragorn: 2931)
sondern nach Schlüsseln geordnet: (Arwen Undomil: 241)
(Bilbo Baggins: 2890)
(Boromir: 2978)
(Frodo Baggins: 2968)
(Merry Brandybuck: 2982)
(Pippin Took: 2990)
(Sam Gamgee: 2980)
529
Beispiel für die Anwendung eines Map-Containers (4)

 Gegeben zwei Paare: VT v1("Pippin Took", 2990);


VT v2("Pippin Took", 4242);

 Dann kann man testen, ob ein Element erfolgreich eingefügt wurde, oder ob es
schon vorhanden ist, und daher nicht nochmals eingefügt werden kann:
if (gt.insert(v1).second) cout << "erfolgreich eingefügt";
else "Fehler beim Einfügen";
cout << endl; erfolgreich eingefügt
 Oder noch ausführlicher:
const pair<GebJahrTabelle::iterator, bool> b = gt.insert(v2);
if (!b.second) cout <<b.first->first << " ist bereits
vorhanden und hat den Wert " << b.first->second << endl;

Pippin Took ist bereits vorhanden und hat den Wert 2990

 Bei Verwendung von [] gibt es natürlich keinen Einfüge-Fehler!


 Der Wert wird jeweils überschrieben: gt["Pippin Took"] = 4242;
gt["Pippin Took"] = 2990;
530
Weitere Methoden und Iteratoren für assoziative Container (1)

 Wir hatten bereits die Methode insert für einen Map-Container diskutiert.
 Diese gibt es in zahlreichen Variationen für alle assoziativen Container.
 Bei Multi-Containern kann man auch mehrere Elemente einfügen.
 Analog gibt es die Methode erase. Der Rückgabewert ist die Zahl der
gelöschten Elemente.

cout << gt.erase("Gandalf") <<' '<< gt.erase("Pippin Took");


0 1

 Weiterhin gibt es die Methode void clear().


 Die Methode find gibt einen Iterator zurück: entweder auf das gefundene
Element oder hinter das Ende des Containers. Argument ist ein Key.

if (gt.find("Gandalf") == gt.end()) cout " … " << endl;

Gandalf nicht gefunden!


531
Weitere Methoden und Iteratoren für assoziative Container (2)

 Die Methode lower_bound liefert einen Iterator auf das erste Element eines
Map-Containers, das nicht kleiner ist als das Key-Argument.
 Die Methode upper_bound liefert einen Iterator auf das erste Element eines
Map-Containers, das größer ist als das Key-Argument.
 Falls es ein solches Suchergebnis nicht gibt, dann wird end() geliefert.
string gandalf = "Gandalf";
GebJahrTabelle::iterator itlb = gt.lower_bound(gandalf);
GebJahrTabelle::iterator itub = gt.upper_bound(gandalf);
cout << " (" << itlb-> first << ": " << itlb->second << ")\n";
cout << " (" << itub-> first << ": " << itub->second << ")\n";
(Merry Brandybuck: 2982)
(Merry Brandybuck: 2982)

 Die Suchergebnisse von lower_bound und upper_bound sind offensichtlich


identisch, wenn es sich um einen Map-Container handelt. Nur bei einem
Multimap-Container können sie verschieden sein.
 Die Methode count liefert die Anzahl der Element eines
Multimap-Containers, deren Schlüssel das Key-Argument ist.
532
Beispiel für die Anwendung von Set-Containern (1)
 In einem Set-Container werden Schlüssel ohne zusätzliche Informationen
verwaltet. Man könnte einen solchen Container auch als vereinfachten Map-
Container betrachten – mit jeweils nicht vorhandenem Wert.
 Wir können eine Menge von Zahlen verwalten z.B. Lotto-Tips:

int main(){
int a[] = { 1, 13, 14, 15, 22, 47, 11, 32, 35, 39, 43, 48,
1, 22, 35, 40, 43, 48, 9, 13, 14, 22, 32, 43 };
int max = sizeof(a) /sizeof(a[0]);
cout << "Inhalt von a: ";
for (int i=0; i<max; i++) cout << a[i] << " "; cout << endl;
set<int> m1;
multiset<int> m2;
for (int i=0; i<max; i++) {
int x = a[i]; m1.insert(x); m2.insert(x);
}
cout << "Inhalt von m1: " << m1;
cout << "Inhalt von m2: " << m2;
}
533
Beispiel für die Anwendung von Set-Containern (2)

 In dem Array a sind die Zahlen der Reihe nach gespeichert.


 In dem Set-Container m1 sind alle vorkommenden Zahlen, jeweils einmal, in
aufsteigender Reihenfolge, gespeichert.
 In dem Multiset-Container m2 sind alle vorkommenden Zahlen, ggf.
mehrfach, in aufsteigender Reihenfolge, gespeichert.

Inhalt von a: 1 13 14 15 22 47 11 32 35 39 43 48 1 22 35 40
43 48 9 13 14 22 32 43
Inhalt von m1: 1 9 11 13 14 15 22 32 35 39 40 43 47 48
Inhalt von m2: 1 1 9 11 13 13 14 14 15 22 22 22 32 32 35 35
39 40 43 43 43 47 48 48

534
Beispiel für die Anwendung von Set-Containern (3)

 Die Ausgabe der Mengen mit dem Operator setzt die folgenden Definitionen
voraus – wie mit Iteratoren bereits gehabt:
#include <iostream>
#include <set>
using namespace std;

template<typename T>
ostream& operator<<(ostream& os, const set<T>& s){
for (set<T>::const_iterator i = s.begin(); i != s.end(); i++)
os << *i << ' ';
os << endl; return os;}

template<typename T>
ostream& operator<<(ostream& os, const multiset<T>& s){
for (multiset<T>::const_iterator // usw. wie oben

 Die Definitionen für multiset wurden bereits mit #include <set> importiert.
 Die Definitionen des Operators << müssen für set und multiset gemacht
werden.
535
Methoden von Set-Containern

 Die Methoden insert, erase, clear, find, count, lower_bound und


upper_bound sind wie gehabt definiert.
 Wir können z.B. testen wie häufig der Key 22 in m2 vorkommt:

cout << "Anzahl von 22 in m2: " << m2.count(22);

Anzahl von 22 in m2: 3

536
Typen von Funktionsobjekten (1)

 In STL werden viele Algorithmen generisch implementiert


Funktionsobjekte werden als Parameter übergeben.
 Die Funktionsobjekte lassen sich wie folgt charakterisieren:
 BinaryOperation
ist ein zweistelliges Funktionsobjekt, das irgendeinen Wert liefert, der i.A. von
beiden Argumenten abhängt.
 BinaryPredicate
ist ein zweistelliges Funktionsobjekt, das einen boolschen Wert liefert, der
i.A. von beiden Argumenten abhängt.
 Compare
steht für ein zweistelliges Funktionsobjekt, das einen boolschen Wert liefert,
i.A. durch einen Vergleich beider Argumente.

Siehe auch: Buch von Kuhlins und Schader


537
Typen von Funktionsobjekten (2)

 Function
bezeichnet Funktionen und/oder Funktionsobjekte, die i.A. ihre Argumente
nicht verändern.
 Generator
ist ein Funktionsobjekt, das irgendwelche Werte generiert.
 Predicate
ist ein einstelliges Funktionsobjekt, das einen booleschen Wert liefert.
 RandomNumberGenerator
ist ein Funktionsobjekt, das zufällige Werte generiert.

Siehe auch: Buch von Kuhlins und Schader


538
Typen von Funktionsobjekten (3)

 Size
ist ein Typparameter für Größenangaben (i.A. ganzzahlig).
 T
Typ von Container-Objekten.
 UnaryOperation
ist ein einstelliges Funktionsobjekt, das einen Wert liefert.

Siehe auch: Buch von Kuhlins und Schader

539
Muster für Funktionsobjekte in STL
 In der STL gibt es zwei Oberklassen, die von den in der STL angebotenen
Funktionen implementiert werden.
template<class Arg, class Result>
struct unary_function

template<class Arg1, class Arg2, class Result>


struct binary_function

 Diese Klassen beinhalten nur Typdeklarationen für die Komposition von


Funktionen.
 Von diesen werden dann die tatsächlichen Klassen abgeleitet:
template<class T>
struct less: binary_function<T,T,bool> {
bool operator()(const T& x, const T& y) const {
return x < y;
}
};

540
Beobachtende Algorithmen
 Beobachtende, also nicht modifizierende Algorithmen können mit
const_Iteratoren aufgerufen werden.
 Beispiel for_each aus der Bibliothek <algorithm>:

template<class InputIterator, class Function>


Function for_each(InputIterator first,
InputIterator last, Function f);

 for_each wird typischerweise benutzt, um für jedes Element, bzw. für einen
Bereich, eines Containers eine Funktion mit dem jeweiligen Element als
Argument aufzurufen.
 Beispiel:
 Wir wollen für jedes Element der Mengen m1 und m2 des Beispiels über die Anwendung
von Set-Containern ausgeben ob es gerade oder ungerade ist.
 Die können wir z.B. mit folgender Funktion erreichen:

void g_oder_u(int i) { cout << ((i % 2 == 0) ? 'g' : 'u'); }

541
Beispiel for_each (1)

 Anwendung: for_each(m1.begin(), m1.end(), g_oder_u);


cout << endl;
for_each(m2.begin(), m2.end(), g_oder_u);
cout << endl;

 Ergebnis: uuuugugguuguug
uuuuuugguggggguuuguuuugg

 Ähnlich könnten wir natürlich auch die Elemente der Menge mit for_each
ausgeben.
 Wir könnten aber auch die Summe aller Elemente mit for_each berechnen.
 Dazu müssten wir ein eigenes Funktionsobjekt definieren, indem die Summe
gespeichert ist. Wir nutzen dabei aus, das for_each das Funktionsobjekt, das
es als Parameter bekommt, als Ergebnis (ggf. modifiziert) zurückgibt.

542
Beispiel for_each (2)
 Beispiel für ein Summen-Funktionsobjekt:
template<class T>
class Summe : unary_function<T, void> {
public:
explicit Summe(const T& t = T()) : sum(t) { }
void operator()(const T& t) { sum += t; }
T summe() const { return sum; }
private:
T sum; // Eine Funktion mit einem Zustand !!
};
 Anwendung:
Summe<int> s1 = for_each(m1.begin(), m1.end(), Summe<int>());
cout << "Die Summe …: " << s1.summe() << endl;
Summe<int> s2 = for_each(m2.begin(), m2.end(), Summe<int>());
cout << "Die Summe …: " << s2.summe() << endl;

Die Summe der Elemente von m1 ist: 369


 Ergebnis: Die Summe der Elemente von m2 ist: 642
543
Weitere beobachtende Algorithmen

 find und find_if: Suchen nach dem ersten Element, das einen bestimmten
Wert hat bzw. ein bestimmtes Prädikat erfüllt.
template <class InputIterator, class Predicate>
InputIterator find_if(InputIterator first,
InputIterator last, Prodicate pred);
 count und count_if: Zählen aller Elemente, die einen bestimmten Wert haben
bzw. ein bestimmtes Prädikat erfüllen.
 search: Suchen nach einem Bereich von Elementen.
 Adjacent_find : Suchen nach Paaren von Elementen, die einen bestimmten
Wert haben.
 usw. usw.

544
Modifizierende Algorithmen

 copy: Kopieren eines Bereiches in einen anderen Bereich.


 remove und remove_if: Entfernen aller Elemente, die einen bestimmten Wert
haben bzw. ein bestimmtes Prädikat erfüllen.
 replace und replace_if: Ersetzen aller Elemente, die einen bestimmten Wert
haben bzw. ein bestimmtes Prädikat erfüllen, durch einen neuen Wert.
 fill: Auffüllen eines Bereichs mit Elementen.
 generate : Generieren von Elementen.
 random_shuffle: bringt die Elemente eines Bereichs in eine zufällige
Reihenfolge.
 transform: Führt für jedes Element eines Bereichs eine ggf. modifizierende
Funktion aus bzw. verknüpft zwei Bereiche mit einer zweistelligen Funktion.
 usw. usw.

545
Weitere Algorithmen

 sort: Sortieren eines Bereichs.


 stable_sort: dito, aber die relative Reihenfolge bleibt erhalten.
 binary_search: Binäre Suche.
 merge: Mischt zwei Bereiche in einen dritten Bereich.
 nth_element: Liefert das n kleinste Element in einem Bereich.
 make_heap, sort_heap, push_heap, pop_heap : Heap-Operationen.
 includes, set_difference, set_intersection, set_union: Mengen-
Operationen.
 usw. usw.

546
Beispiel für Transform

 Mit den Mengen m1 und m2 können wir ein Beispiel für transform bilden.
 Wir definieren zunächst eine einstellige Funktion zum Negieren:

int neg(int i) { return -i; }

 Dann wenden wir diese Funktion auf jedes Element von m2 an und lassen
das Ergebnis eine Kopie m3 von m2 ersetzen:

transform(m2.begin(), m2.end(), m3.begin(), neg);


cout << "Inhalt von m2: " << m2;
cout << "Inhalt von m3: " << m3;

Inhalt von m2: 1 1 9 11 13 13 14 14 15 22 22


 Ergebnis: 22 32 32 35 35 39 40 43 43 43 47 48 48
Inhalt von m3: -1 -1 -9 -11 -13 -13 -14 -14 -
15 -22 -22 -22 -32 -32 -35 -35 -39 -40 -43 -
43 -43 -47 -48 -48

547
Generieren zufälliger Vektoren (1)

 Wir wollen Vektoren mit zufälligem Inhalt erzeugen.


 Dazu definieren wir einen eigenen Zufallszahlen-Generator, der mit denselben
Parametern immer dieselben Folgen erzeugt.
 Zufall<int>(7, 20) erzeugt einen Zufallszahlen-Generator mit dem Startwert
7, der eine Folge von int-Zufallszahlen im Bereich 0 bis 19 erzeugt.
template<class T>
class Zufall {
public:
Zufall(T start = 0, T max = 10) : x(start), n(max) { }
T operator()() { return (x = (138574735 * x + 7)) % n; }
private:
unsigned long x;
unsigned int n;
};

 In den folgenden Folien verwenden wir noch folgende Typdefinitionen:


typedef vector<int> Vi;
typedef vector<int>::iterator Vit;
548
Generieren zufälliger Vektoren (2)

 Wir nutzen die bisherigen Definitionen um drei Vektoren v1, v2 und v3 zu


definieren. Diese haben jeweils 10 Elemente.
 v1 und v2 werden mit dem Zufallszahlen-Generator mit Werten gefüllt:
Vi v1(10), v2(10), v3(10);
Vit ib1 = v1.begin(), ib2 = v2.begin(), ib3 = v3.begin();
Vit ie1 = v1.end(), ie2 = v2.end(), ie3 = v3.end();

generate(ib1,ie1,Zufall<int>(7, 20));
generate(ib2,ie2,Zufall<int>(19, 30));

cout << "Inhalt von v1:" << endl << v1;


cout << "Inhalt von v2:" << endl << v2;

 Ergebnis: Inhalt von v1:


12 19 8 15 0 7 16 11 16 11
Inhalt von v2:
2 7 24 19 22 21 20 13 24 9

549
Anwendung von transform auf diese Vektoren

 Wir nutzen die drei Vektoren v1, v2 und v3 der letzten Folie, um mit dem
Algorithmus transform die elementweise Summe v3 = v1 + v2 zu bilden.

cout << "Inhalt von v1:" << endl << v1;


cout << "Inhalt von v2:" << endl << v2;
transform(ib1, ie1, ib2, ib3, plus<int>());
cout << "Inhalt von v3:" << endl << v3;

Inhalt von v1:


 Ergebnis: 12 19 8 15 0 7 16 11 16 11
Inhalt von v2:
2 7 24 19 22 21 20 13 24 9
Inhalt von v3:
14 26 32 34 22 28 36 24 40 20

550
Beispiel für Sort (1)

 Analog zu den vorigen Folien definieren wir einen Vektor v mit hundert
zufälligen Elementen: Ausgabe v (1).

Vi v(100);
Vit ib = v.begin();
Vit ie = v.end();

generate(ib,ie, Zufall<int>(17, 100));


cout << "Inhalt von v (1):" << endl << v;

 Ergebnis:

Inhalt von v (1):


2 85 34 89 94 13 58 73 66 49 86 49 2 37 10 53 42 5 66 5 42 5 54
9 94 97 98 93 70 93 54 81 62 41 74 53 6 65 30 97 46 45 50 89 34
73 66 53 10 5 42 9 2 45 18 49 86 89 38 97 22 97 2 61 78 57 90
13 82 37 10 41 46 57 18 17 22 49 82 45 18 17 2 41 54 1 46 93 90
45 90 9 58 53 74 1 22 45 2 29

551
Beispiel für Sort (2)

 Im nächsten Schritt nutzen wir den Algorithmus random_shuffle, um die


Elemente des Vektors nochmals durchzumischen, also in eine andere
zufällige Reihenfolge zu bringen : Ausgabe v (2).

random_shuffle(ib,ie);
cout << "Inhalt von v (2):" << endl << v;

 Ergebnis:

Inhalt von v (2):


2 85 49 2 22 93 38 2 46 90 66 2 62 46 41 9 9 45 74 5 66 53 78
90 57 73 58 22 13 29 49 17 18 53 86 97 97 89 93 42 97 82 10 37
94 49 45 5 42 97 17 41 5 70 82 53 42 74 6 34 41 30 66 22 54 1
89 54 65 9 10 45 81 45 57 58 49 34 90 50 54 2 46 45 13 93 1 89
18 37 73 98 53 86 10 18 2 61 5 94

 Erstaunlicherweise ändern die ersten beiden Elemente ihre Position nicht!

552
Beispiel für Sort (3)

 Anschließend wenden wir den Algorithmus sort, um die Elemente des


Vektors in aufsteigender Reihenfolge zu sortieren: Ausgabe v (3).

sort(ib,ie);
cout << "Inhalt von v (3):" << endl << v;

 Ergebnis:

Inhalt von v (3):


1 1 2 2 2 2 2 2 5 5 5 5 6 9 9 9 10 10 10 13 13 17 17 18 18 18
22 22 22 29 30 34 34 37 37 38 41 41 41 42 42 42 45 45 45 45 45
46 46 46 49 49 49 49 50 53 53 53 53 54 54 54 57 57 58 58 61 62
65 66 66 66 70 73 73 74 74 78 81 82 82 85 86 86 89 89 89 90 90
90 93 93 93 94 94 97 97 97 97 98

553
Beispiel für Sort (4)

 Abschließend rufen wir eine Variante des Algorithmus sort auf, um die
Elemente des Vektors in absteigender Reihenfolge zu sortieren:
Ausgabe v (4).

sort(ib,ie, greater<int>());
cout << "Inhalt von v (4):" << endl << v;

 Ergebnis:

Inhalt von v (4):


98 97 97 97 97 94 94 93 93 93 90 90 90 89 89 89 86 86 85 82 82
81 78 74 74 73 73 70 66 66 66 65 62 61 58 58 57 57 54 54 54 53
53 53 53 50 49 49 49 49 46 46 46 45 45 45 45 45 42 42 42 41 41
41 38 37 37 34 34 30 29 22 22 22 18 18 18 17 17 13 13 10 10 10
9 9 9 6 5 5 5 5 2 2 2 2 2 2 1 1

554
Einfache Funktionskomposition

 In der STL ist es möglich, neue Funktionen auf Basis anderer


Funktionen zu erzeugen (Komposition).
Umwandlung von einer zweistelligen Funktion in eine einstellige
Funktion durch Wertzuweisung an einen Parameter.
 bind2nd(less<int>(), 100)
Hierdurch wird durch die Funktion geprüft, ob der Wert des
ersten Parameters kleiner 100 ist.
 bind1st
Entspricht bind2nd, nur dass eine Konstante an den ersten
Parameter gebunden wird.
Modifikation von Boolschen Funktionen
 Durch die Funktion not1 wird eine einstellige Boolsche
Funktion negiert.
not1(bind2nd(less<int>(), 100))
Diese Funktion überprüft, ob der Wert des Parameters nicht
kleiner als 100 ist.

555
Weitere Funktionalität

 Binäre Suche, Lineare Suche


 Partitionieren eines Iterators in zwei
 Mengenoperationen
 Minus, Schnitt, symmetrische Differenz, Vereinigung
 Sortieren eines Iterators
 Heaps über Random Access Iteratoren
 Numerische Funktionen
 Berechnung eines inneren Produkts
 Verschmelzen von Iteratoren
 auch von sortierten Folgen

556
C++-Potpourri

 Übersicht
 Namensräume
 Konvertierungsfunktionen
 Typinformation zur Laufzeit (RTTI)
 Assoziierte Typen und Traits

557
Namensräume

 Namensräume definieren Sichtbarkeitsbereiche, um


zusammengehörige Namen zu gruppieren.
 Namensräume vermeiden Konflikte zwischen gleichnamigen,
globalen Elementen durch Erweiterung der enthaltenen Namen um
einen zusätzlichen, einheitlichen Namen.
 Definition eines Namensraums

namespace identifier {
// namespace-body
}

 Zugriff auf Elemente eines Namensraums namespace hallo {


int x = 42;
}
namespace :: element
int x = 23;
int y = x + hallo::x;

558
Anonyme Namensräume

 Motivation
 Begrenzung des Zugriffs einer Variable, Funktion oder Klasse auf
eine Quelldatei
 Ersatz von C’s globalen static Deklarationen
 Beispiel
namespace { // ohne Namen
int x = 0; // private Variablen
void putValue (int value) { // private Methoden
x = value;
}
}
// implizite Nutzung des anonymen Namensraum
 Elemente eines anonymen Namensraum können nur in der
gleichen Datei genutzt werden.
 putValue (77);

559
Globaler Namensraum

 Alle Deklaration, die nicht explizit in einem Namensraum


stehen, werden implizit dem globalen Namensraum
zugeordnet.
 Der Zugriff auf den globalen Namensraum erfolgt durch

::insert ()

560
using-Anweisung

 Durch die using-Anweisung directive werden alle Elemente eines


Namens importiert:
using namespace Comp; // nicht immer empfehlenswert
Angestellte s; // Bedeutung: "Emp::Angestellte s;“

 Der Zugriff auf die Element erfolgt analog zu lokalen.


 Namenskonflikte
 Lokale Namen werden im Fall eines Konflikts stets bevorzugt.

 Import eines spezifischen Elements aus einem Namensraum


using namespace Comp::Angestellte; // besser
Angestellte s;

 Namenskonflikte
 Übersetzungsfehler

561
Alias

 Motivation
 Verwendung eindeutiger Namen für Namensräume
 Vereinfachung von Namen
 Beispiel
namespace CompanyFromSomewhere { ... }
namespace Company = CompanyFromSomewhere; // alias
Company::Employee e; // use alias

562
Erweiterung von Namensräumen

 Ein Namensraum kann durch Redefinition eines


Namensraums mit dem gleichen Namen beliebig erweitert
werden.
 Beispiel:

namespace Company { // Erweiterung des Namensraum


class Volunteer { // Definition einer neuen Klasse
...
};
}

 Dadurch kann die Definition eines Namensraum über


verschiedene Dateien verteilt werden.

563
Zusammenfassung

 Logisch zusammengehörende Elemente sollen im gleichen


Namensraum liegen.
 Statt der einfachen using-Anweisung sollte die explizite
Deklaration der member-Elemente verwendet werden.
 Header-Dateien sollte keine using-Anweisung beinhalten.
 Stattdessen Nutzen von vollqualifizierten Namen

564
Beispiele

namespace first namespace first


namespace first { int x = 54; } { int x = 54; }
{ int x = 54; } … …
… namespace first namespace first
namespace first { int y = 44; } { int y = 44; }
{ int y = 44; }
namespace second = first; namespace second = first;
int main()
{ int main() int main()
using namespace first; { {
int a = x; // a == 54; using namespace second; int a = first::x; // a == 54;
return 0; int a = x; // a == 54; int b = second::x; // b == 54;
} return 0; return 0;
} }

565
Konvertierungsfunktion (1)

 Ziel
 Verwendung eines Objekts einer Klasse in einem Ausdruck, wo ein
Objekt einer anderen Klasse stehen sollte.
 Definition einer Konvertierungsfunktion
class stack {
int s[];
int numb;
public:
stack() {numb = 0;}

void push(int i);
operator int() { return numb;}
// Konvertierungsfunktion: stack  int

}

566
Konvertierungsfunktion (2)

 Verwendung der Konvertierungsfunktion


int main() {
stack s;
for (int i = 0; i < 20; i++} s.push(i);
int j = s; // Konvertierung
cout << MaxSize – s << " Plätze noch frei\n";
}

567
Explizite Konstruktoren

 Durch einstellige Konstruktoren werden ebenfalls


Konvertierungsfunktionen bereitgestellt.
 Ein Aufruf von
MyClass ob = 4;
wird dann automatisch in
MyClass ob(4);
transformiert.
 Verhinderung dieses Mechanismus durch explizite
Deklaration der Konstruktoren.
class MyClass {
int a;
public:
explicit MyClass(int x) {a = x;}

}

568
Typinformation zur Laufzeit

 Motivation
 Auf Grund der Polymorphie bei objektorientierten Sprachen ergibt
sich das Problem, dass der Typ eines Objekts nicht bekannt ist.
 RTTI ist ein Mechanismus in C++, um Typinformation zur Laufzeit zu
erzeugen.
 Anwendung
 Import von <typeinfo>
 Aufruf der Funktion typeid(object) bzw. typeid(type)
 Liefert ein Objekt der Klasse type_info
 Funktionalität der Klasse type_info
 Test auf Gleichheit mit einem anderen Typ
 Überschriebene der Operatoren == und !=
 Ausgabe des Typnamens
 Methode const char *name()

569
Beispiel

#include <iostream> int main() {


#include <typeinfo> int i;
using namespace std;
printTN("i",i);
class A { A a1 = B();
public: printTN("a1",a1);
class C { A *a2 = new A();
}; printTN("a2",a2);
};
B *b = new B();
class B : virtual public A {};
template <typename T> class D{}; printTN("b",b);
A::C c = A::C();
template <typename T> printTN("c",c);
void printTN(char *s, T t){ D<int> d = D<int>();
cout << "Der Typ von "
printTN("d",d);
<< s
<< " ist: " return 0;
<< typeid(t).name() }
<< endl;
}

570
Cast-Operatoren

 In C++ gibt es 5 verschiedene Cast-Operatoren


 Es gibt immer noch den bekannten Cast-Operator aus C.
 Unter den restlichen 4 Operatoren ist dynamic_cast am wichtigsten.
 Motivation: Konvertierung polymorpher Typen
 Syntax: dynamic_cast<target_type> (expr)
 target_type ist der Zieltyp des Ausdrucks expr, wobei Zieltypen nur
Pointer- bzw. Referenztypen sein können.
 Beispiel:

class A {
public:
virtual ~A(){}
};

class B : public A {};

int main() {
B b = B();
A *a = &b;
B *b2 = dynamic_cast<B *> (a);
}
571
Weitere Cast-Operatoren

 const_cast
 Motivation: Konvertierung von Konstanten in normale Variablen
 ACHTUNG: Benutzung nur mit allergrößter Vorsicht!
 Syntax const_cast<type> (expr)
 static_cast
 Nicht-polymorphe Typkonvertierung
 entspricht dem klassichen Cast-Operator aus C
 Syntax: static_cast<type> (expr)
 reinterpret_cast
 Konvertierung beliebiger Typen: Hier stellt sich die Sinnfrage?
 Syntax: reinterpret_cast<type> (expr)

572
Traits

 Templates besitzen oft sehr viele Parameter, was dazu führt,


dass die Instanziierung des Templates sehr kompliziert wird.
 Häufig lässt sich jedoch beobachten, dass ein Typ funktional die
anderen Typen bestimmt.
 Diese Beziehung lässt sich nun mit Template-
Programmierung, sogenannter Traits, ausdrücken.

 Zur Erinnerung
 Eine Funktion liefert zu einem Wert einen Wert.
 Im Unterschied dazu:
 Ein Trait liefert ähnlich zu der Funktion sizeof() zu einem Typ
einen oder mehrere andere Werte oder Typen

573
Motivation

 Betrachten wird die folgende Template-Funktion:


template <typename T>
inline T accum (T const* beg, T const* end) {
T total = T(); // T() liefert den Wert 0
while (beg != end) {
total += *beg;
++beg;
}
return total;
}
 Dies Funktion lässt sich für Arrays aller Typen aufrufen.
 Problemfall:
char str[] = "Programmieren“;
accum(&name[0], &name[12]) / 13;
 Der Typ char ist zu klein
574
Lösung (1)

 Einführung eines neuen Template-Parameters, der bei jedem


Aufruf spezifiziert werden muss.
 i. A. zu aufwändig
 Alternative: generische Typen
template<typename T> class AccumulationTraits;

// Spezialisierungen von AccumulationTraits


template<> class AccumulationTraits<char> {
public: typedef int AccT;
};
template<> class AccumulationTraits<short> {
public: typedef int AccT;
};
template<> class AccumulationTraits<int> {
public: typedef long AccT;
};

575
Lösung (2)

 Unsere Funktion kann nun folgendermaßen implementiert


werden:

template <typename T>


// Rückgabetyp ist jetzt der bulid-in-Typ der Klasse AccumulationTraits
inline typename AccumulationTraits<T>::AccT
accum (T const* beg, T const* end) {
// Redefinition des Typs AccT als lokaler Typ
typedef typename AccumulationTraits<T>::AccT AccT;

AccT total = AccT(); // T() liefert den Wert 0


while (beg != end) {
total += *beg; ++beg;
}
return total;
}

576
Mehrdimensionale Traits

 Statt von einem Typparameter können nun auch mehrere Typen


einen anderen Typ bestimmen.
 Beispiel
 Wir betrachten im Folgenden die skalare Addition von zwei
Vektoren:
 template<typename T> Array<T>
operator+ (Array<T> const&, Array<T> const&);
 Jetzt soll die Funktion so erweitert werden, dass man auch
ein Array von char mit einem Array von int addieren kann.
 template<typename T1, typename T2>
Array<???> operator+ (Array<T1> const&, Array<T2>
const&);
 Frage ist jedoch nach dem Typ der Ausgabe?

577
Ein Beispiel
#include <iostream> // For cout, manipulators.

 Ziel
#include <list> // For lists.
#include <vector> // For vectors.
#include <iterator> // For ostream iterator.
using namespace std;  Ausgabe unterschiedlicher
Datenmengen
 Erste Lösung
int main (int, char *[]) {

list<char> l;
l.push_back ('a');  Listen und Vektoren
l.push_back ('b');
l.push_back ('d');  Iterator für Ausgabe
cout << "list l contains: ";  Ausgabe nach cout
ostream_iterator<char> osic (cout, " ");
copy (l.begin(), l.end(), osic);  Funktion copy
cout << endl;  Datenbereich durch
vector<int> v;
Iteratoren
v.push_back (1);  Codeduplizierung
v.push_back (2);
v.push_back (4);  Anforderungen
cout << "vector v contains: ";
ostream_iterator<int> osii (cout, " ");  Codevereinheitlichung
copy (v.begin(), v.end(), osii);
cout << endl;
 noch generischer
return 0;
}

578
Überlegungen
#include <iostream> // For cout, manipulators.  Copy-Funktion
#include <list> // For lists.
#include <vector> // For vectors.  3 Iteratoren
#include <iterator> // For ostream iterator.
using namespace std;  2 für Input
 1 für Output
 Typ der Inputiteratoren
int main (int, char *[]) {

 Können wir diese von den


list<char> l;
l.push_back ('a');
l.push_back ('b');
l.push_back ('d'); Containern bekommen?
cout << "list l contains: ";
ostream_iterator<char> osic (cout, " ");  Falls ja, wie?
copy (l.begin(), l.end(), osic);
cout << endl;  Typ des
vector<int> v; Outputiterators
v.push_back (1);
v.push_back (2);
 Können wir den Parameter
v.push_back (4); vom Input-Iterator
cout << "vector v contains: ";
bekommen?

ostream_iterator<int> osii (cout, " ");
copy (v.begin(), v.end(), osii);
cout << endl;
Wenn ja, wie?
return 0;
}

579
Container → Iterator (Typen)

template <class T, size_t N>


struct block{
typedef T value_type;
typedef value_type * pointer;
typedef value_type & reference;
typedef const_value_type* const_pointer;
typedef const value_type & const_reference;
typedef size_t size_type;
……
typedef pointer iterator;
typedef const_pointer const_iterator;
……
};
Jeder STL-Container bietet diese Typen an!

580
Container → Iterator (Instanzen)

template <class T, size_t N>


struct block{

……

iterator begin() {return data;}


iterator end() {return data+N;}
const_iterator begin() const {return data;}
const_iterator end() const {return data+N;}

……
Jeder STL-Container erzeugt Iteratoren des
T data [N]; korrekten Typs.
};

581
Container → Iterator → Trait → Type

 Generische print-
#include <iterator> Funktion für Container
#include <algorithm>
 Container-Typ T als
using namespace std; Template-Parameter
template <typename T>
 Funktionsparameter t als
const Referenz
 Assoziierte Typen
void print_container (const T & t) {

ostream_iterator
<typename T::value_type>  vom Container-Typ
osi (cout, " ");  Verwendung von
typename
 Verwendung von
copy (t.begin(), t.end(), osi);

cout << endl;


Iteratoren
 t.begin() undt.end()
}

582
Anwendung
#include "print_container_T.h"  Program ist einfacher
#include <iostream> // For cout.
#include <list> // For lists.
 Funktioniert für beliebige
Container
#include <vector> // For vectors.
using namespace std;  Details der Ausgabe in der
Funktion print_container.
int main (int, char *[]) {
 Problem
list<char> l;  Beschränkt auf Container 
l.push_back ('a'); Keine Unterstützung von
l.push_back ('b'); Standardtypen, z. B. char[]
l.push_back ('d');
cout << "list l contains: ";
print_container (l);
 Verbesserung
vector<int> v;
v.push_back (1);
 Iterator-basierte
Schnittstelle
 Definition eigener Trait-
v.push_back (2);
v.push_back (4);
cout << "vector v contains: ";
print_container (v);
Klassen.
return 0;
}

583
Deklaration eigener Traits

template <typename T>  Typische Muster für


struct print_traits { Traits
};
typedef typename T::value_type value_type;  Partielle Instanziierung
für const und non-const
Zeiger
// partial specialization for pointers
template <typename T>  Eigene assoziierte
struct print_traits<T *> { Typen
typedef T value_type;  Basis für ein noch
}; generischeres Funktion-
Template
 Eigentlich hätte man
// partial specialization for const pointers
template <typename T>
struct print_traits<const T *> { hier auch auf die
typedef T value_type; Traits in der STL
}; zurückgreifen
können.

584
Anwendung der Traits in
einer Template-Funktion

#include "print_T.h"
 Generische print Funktion
#include <iterator>
#include <algorithm>
 Iteratoren als Input-Parameter des
Templates
using namespace std;
 Iteratoren als Parameter der Funktion
 Definition eines Bereichs
template <typename I>  Verwendung von typedef
void print (I i, I j) {  Deklaration eines lokalen Typnamens
 Dies ist nicht notwendig, führt aber zu
typedef typename lesbarerem Code
print_traits<I>::value_type
VTYPE;
 Generische Anwendung für
jegliche Art von Iteratoren
ostream_iterator<VTYPE>  const und non-const Pointer werden
osi (cout, " ");
ebenfalls unterstützt.

copy (i, j, osi);


cout << endl;
}

585
Nutzen der
generischen Funktion
#include "print_T.h"
#include <iostream> // For cout, manipulators.
#include <list> // For lists. Ausgabe:
#include <vector> // For vectors. list l contains: a b d
#include <cstring> // For strlen. vector v contains: 1 2 4
using namespace std;
int main (int, char *[]) { C-style string s contains:
h e l l o , w o r l d !
list<char> l; l.push_back ('a'); Const double precision floating
l.push_back ('b'); l.push_back ('d');
point number d contains: 3.141
cout << "list l contains: ";
print (l.begin(), l.end());

vector<int> v; v.push_back (1);


 Programm ist einfach
v.push_back (2); v.push_back (4); geblieben!
cout << "vector v contains: ";
print (v.begin(), v.end ());
 Größerer
Funktionsumfang
char * s = "hello, world!";  Arrays
cout << "C-style string s contains: " << endl;
print (s, s + strlen(s));
 Variablen
const double d = 3.141;
cout << "Const double precision floating \n"
" point number d contains: ";
print (&d, (&d)+1);
return 0;
}
586
Zusammenfassung

 Vielseitiger Nutzen von assoziierten Typen in C++


 Z. B. in Templates
 Einfachere Programme (mit weniger Code)
 Konsistente Typbezeichner durch typedefs in
Klassen
 Definition assoziierter Typen
 Übersichtlicher Code
 Traits beseitigen Probleme mit assoziierten Typen
 Unter benutzer-definierten, const, non-const Typen
 Konsistene Nutzung generischer Funktionen
587
Entwurfsmuster

 Seit im Jahre 1995 das Buch “Design Patterns: …” von Erich


Gamma, Richard Helm, Ralph Johnson und John V. lissides
erschien, ist das Thema in aller Munde.
 Zuvor wurde dieses Thema von vielen Personen bereits
angerissen.
 Software Entwurfsmuster
 Beschreibung einer Familie von Lösungen für ein Software
Entwurfsproblem.
 Eigenschaften von Muster
 Verbesserung der Kommunikation, da ein Muster eine
Kurzformel für ein komplexes Modell ist, das direkt in eine
Implementierung umgesetzt werden kann.
 Erfassung wesentlicher Aspekte bei der Modellierung
 Dokumentation von Lösungen
 Muster verbessern den Programmcode
 robuster gegenüber Änderungen

588
Beschreibungsstruktur für ein Muster

 Namen
 Kurzbeschreibung der wesentlichen Aspekte
 Beschreibung des Problems, das durch das Muster gelöst wird.
 Struktur: Komponenten und deren Beziehungen
 Interaktion
 Konsequenzen
 Implementierung
 Beispielcode

Unterscheidung der Muster in drei Kategorien


 Muster für die Erzeugung von Objekten (creational patterns)
 Strukturelle Muster (structural patterns)
 Verhaltensbasierte Muster (bahavioral patterns)

589
Klassifikation von Entwurfsmuster

Erzeugende Muster Strukturelle Muster Verhaltensmuster

Factory Method Adapter Interpreter


Abstract Factory Bridge Template Method
Builder Composite Chain of
Prototype Decorator Responsibility
© E. Gamma, R. Helm, R. Johnson, J. Vlissides and Addison-Wesley

Singleton Flyweight Command


Facade Iterator
Proxy Mediator
Memento
Observer
State
Strategy
Visitor

590
Observer-Muster

 Zweck
 Definition einer 1:n-Abhängigkeit zwischen Objekten
 Bei Änderung des Zustands eines Objekts werden alle
abhängigen Objekte darüber informiert.

 Anwendung
 Wenn ein Konzept zwei Aspekte hat, wo der eine von dem anderen
abhängt. Wenn eine Änderung eines Objekts sich auf eine
unbekannte Anzahl von anderen Objekte auswirkt.
 Wenn ein Objekt seine Zustandsänderungen anderen Objekten
mitteilt ohne diese Objekte zu kennen.

 Andere Bezeichnungen
 publish/subscribe

591
Observer (2)

 Struktur

Subject
-observers Observer
© E. Gamma, R. Helm, R. Johnson, J. Vlissides and Addison-Wesley

+attach(in o : Observer)
+detach(in o : Observer) 1 * +update()
+notify()
{for all o in observers
{ o.update() } }

ConcreteSubject -subject ConcreteObserver


-subjectState -observerState
+getState() 1 * +update()

{return subjectState } {observerState =


subject.getState() }

592
Observer (3)

 Folgerungen
 Modularität: Subjekt und Beobachter können unabhängig sein.
 Erweiterbarkeit: Anmeldung beliebig vieler Beobachter
 Anpassung: verschiedene Observer besitzen unterschiedliche
Sichten auf das Subjekt
 Unerwartete Updates: Verschiedene Beobachter sind unabhängig
© E. Gamma, R. Helm, R. Johnson, J. Vlissides and Addison-Wesley

voneinander
 Update Kosten können hoch sein (Tipps für geschickte Strategien)
 Implementierung
 Abbildung Subjekt/Beobachter
 Dangling references
 Vermeiden von spezifischen Update-Protokollen
 Explizite Registerierung der Änderungenswünsche

593
Observer (4)

 Einsatzbereich
 Smalltalk: model-view-controller (MVC)
 Worklflow- und Datenstromsysteme
 Datenbanken: Anpassung von materialisierten Sichten
 Kopplung von Altsystemen (legacy systems) mit neuen Systemen


© E. Gamma, R. Helm, R. Johnson, J. Vlissides and Addison-Wesley

Vorteil
 Vielseitige Wiederverwendung des Designs
 Automatisches Propagieren von Änderungen

594
Observer: Beispiel

Beobachter

Subjekt

595
Komposition

 Zweck
 Gleiche Behandlung von individuellen und zusammengesetzten Objekte
 Anwendung
 Rekursiver Aufbau von Objekten
 Beispiel: Bäume
 Keine Unterscheidung zwischen individuellen und zusammengesetzten
Objekte
© E. Gamma, R. Helm, R. Johnson, J. Vlissides and Addison-Wesley

 Gleiche Behandlung aller Objekte in der Struktur


 Teilehierachie von Objekten

596
Komposition (2)

 Struktur

Component
*
+operation()
+add(in c : Component)
-children
+remove(in c : Component)
© E. Gamma, R. Helm, R. Johnson, J. Vlissides and Addison-Wesley

+getChild(in i : int)

Leaf Composite

+operation() +operation()
+add(in c : Composite)
1
+remove(in c : Composite)
+getChild(in i : int)

{ forall g in children
g.operation(); }

597
Komposition (3)

 Konsequenzen
+ Uniformität: Komponenten werden unabhängig von ihrer Komplexität gleich
behandelt
+ Erweiterbarkeit: neue Unterklassen von Component können eingebracht
werden
* Overhead: hohe Anzahl von Objekten
 Implementierung

© E. Gamma, R. Helm, R. Johnson, J. Vlissides and Addison-Wesley

Kennen Komponenten ihre Eltern?


 Gleiche Schnittstelle für Blätter und interne Knoten?
 Verantwortung für das Löschen von Kindern

598
© E. Gamma, R. Helm, R. Johnson, J. Vlissides and Addison-Wesley

599



Anwendung

Macro-Befehle
Binäre Suchbäume
Komposition (4)
Komposition (5)

600
Komposition– Beispiel

Currency CompositeEquipment::NetPrice() {
Iterator<Equipment*>* i = getIterator();
Currency total = 0;
for (i->First(); !i->IsDone(); i->Next())
total += i->CurrentItem()->NetPrice();
delete i;
return total;
}

// and in the client code … (e.g. in main)

Cabinet* cabinet = new Cabinet("PC Cabinet");


Chassis* chassis = new Chassis("PC Chassis");
cabinet->Add( chassis );
Bus* bus = new Bus ("MCA Bus");
bus ->Add( new Card("16Mbs Token Ring") );
chassis->Add( bus );
chassis->Add( new FloppyDisk("3.5 floppy") );
cout << chassis->NetPrice() << endl;

601
Strategie

 Zweck
 Definition einer Familie von Algorithmen, auf die indirekt über eine
Schnittstelle zugegriffen werden kann.
 Austauschbares Benutzen eines der Algorithmen durch ein Klient

 Anwendung

© E. Gamma, R. Helm, R. Johnson, J. Vlissides and Addison-Wesley

Wenn eine Anwendung aus einer Menge von Algorithmen wählen


kann und diese über eine Schnittstelle zugreifbar sind.

602
Strategie (2)

 Struktur
© E. Gamma, R. Helm, R. Johnson, J. Vlissides and Addison-Wesley

Context (Composition) Strategy (Compositor)

+contextInterface() 1 1 +algorithmInterface()

ConcreteStrategyA ConcreteStrategyB ConcreteStrategyC

+algorithmInterface() +algorithmInterface() +algorithmInterface()

603
Strategie (3)

 Folgerungen
+ Flexibilität und Wiederverwendung
+ Dynamisches Ändern von Algorithmen
 Mehraufwand bei der Kommunikation
 Inflexible Schnittstelle für die Strategie
 Implementierung
 Austausch von Informationen zwischen der Strategie und dem
© E. Gamma, R. Helm, R. Johnson, J. Vlissides and Addison-Wesley

Kontext
 Auswahl der Strategie via Templates
 Beispiele für die Benutzung
 Aufspalten eines Knotens in einem R-Baum
 Ablaufsteuerung und Speicherallokation

604
Decorator

 Zweck
 Anreicherung von Objekten mit neuer Funktionalität

 Anwendung
 Wenn das Konzept der Klassenerweiterung nicht praktikabel ist.

© E. Gamma, R. Helm, R. Johnson, J. Vlissides and Addison-Wesley

Wenn Funktionalität nur temporär zur Verfügung stehen soll.

605
Decorator (2)

 Struktur

Component (Glyph) 1
© E. Gamma, R. Helm, R. Johnson, J. Vlissides and Addison-Wesley

+operation() -component

ConcreteComponent Decorator (MonoGlyph)


{ component-> component.operation(); }
+operation() +operation() 1

ConcreteDecoratorB
ConcreteDecoratorA
{ super.operation();
-addedState addedBehavior(); }
+operation()
+operation()
+addedBehavior()

606
Decorator (3)

 Ein Decorator, auch Wrapper genannt, ist ein Objekt mit der
gleichen Schnittstelle wie die seines enthaltenen Objekts.
 Alle Aufrufe werden an das enthaltene Objekt delegiert.
 Der Decorator stellt darüber hinaus neue Funktionalität zur
Verfügung
 Somit wird durch ein Decorator neue Funktionalität zu einem Objekt
hinzugefügt (und nicht zu einer Klasse)

 Durch Vererbung kann nur für eine Klasse die Funktionalität


erweitert werden, nicht aber für individuelle Objekte.

607
Decorator (4)

 Konsequenzen
+ Funktionalität kann zur Laufzeit einem Objekt hinzugefügt werden.
+ Vermeidung von vielen Klassen
+ Rekursive Anwendung möglich
 Schnittstellen bieten nur ein Teil der Funktionalität
 Verschleierung der Identität
 Implementierung
© E. Gamma, R. Helm, R. Johnson, J. Vlissides and Addison-Wesley

 Konform mit Schnittstelle


 Nutzen einer einfachen abstrakten Klasse für den Dekorator
 Basisklasse sollte möglichst schwergewichtig sein (viel Funktionalität)

608
Decorator Beispiel (Java)

609
Decorator Beispiel (2)

class Book extends LibItem{ public int copiesOnShelf() {


String author; return onShelf;
intnoCopies; }
intonShelf; public void decCopiesOnShelf() {
String title; onShelf--;
public Book(String t, }
String a, int c){ public void incCopiesOnShelf() {
title = t; onShelf++;
noCopies= c; }
author = a; public String getAuthor() {
onShelf= c; return author;
} }
public String getTitle() public void borrowItem(String
{
borrower) {
return title;
System.out.println("borrow–
}
in Book");
public int getCopies() {
}
return noCopies;
public void returnItem(String
}
borrower) { }
public void reserve(String
reserver) { }
} /* End class Book */
610
Decorator Beispiel (3)

abstract class Decorator extends


LibItem{
LibItem item;
public Decorator(LibItem li) {
item = li;
}
public String getTitle() {
return item.getTitle();
}
// Following methods are
// similarly implemented
public String getAuthor() {...
public intgetCopies() { ...
public intcopiesOnShelf() {
...
public void decCopiesOnShelf()
{
item.decCopiesOnShelf();
}
// and similarly...
public void incCopiesOnShelf()
{...
611
Decorator Beispiel (4)

612
Decorator Beispiel (5)
// Non borrowable, non reservablebook
LibItem b1 = new Book("A", "B", 1);
// Borrowablevideo
LibItem v1 = new BorrowableDec(new Video("V", 3));
// borrow unborrowableitem -copies should stay 1
b1.borrowItem("Bob");
System.out.println("Copiesof book = " +b1.copiesOnShelf());
// borrow video -copies decremented to 2
v1.borrowItem("Fred");
System.out.println("Copiesof video = " +v1.copiesOnShelf());
//make book borrowableand borrow it -copies = 0
LibItem b2 = new BorrowableDec(b1);
b2.borrowItem("Bob");
System.out.println("Copiesof book = " +b2.copiesOnShelf());
// make book reservable
LibItem b3 = new ReservableDec(b2);
b3.reserve("Alice");
b3.returnItem("Bob");
// book returned -back to 1 copy
System.out.println("Copiesof book = " +b3.copiesOnShelf());
// Not reserved for Jane -still 1 copy
b3.borrowItem("Jane");
System.out.println("Copiesof book = " +b3.copiesOnShelf());
// Okay Alice can borrow -down to 0
b3.borrowItem("Alice");
System.out.println("Copiesof book = " +b3.copiesOnShelf());
613
Muster für die Erzeugung

 Abstraktion des Erzeugungsprozesses eines Objektes


 Unabhängigkeit von der konkreten Erzeugung der Objekte
 Relevanz, wenn statt Vererbung öfters Komposition benutzt
wird.
 Ziel bei der Objekterzeugung
 Verbergen der konkreten Klasse, von der das Objekt erzeugt
wird.
 Verbergen, wie ein Objekt erzeugt wird.
 Flexibilität bzgl. dem Was, Wie, Wer und Wann
 Folgende Muster werden in der Literatur diskutiert
 Abstrakte Fabrik (Factory / Abstract Factory)
 Prototyp (Prototype)
 Einzelstück (Singleton)
 Erbauer (Builder)

614
Abtrakte Fabrik

 Dieses Muster stellt eine Erweiterung von Fabrik dar. Man geht
davon aus, dass verschiedene plattformabhängige Klassen
existieren.
 Man denke z. B. an die verschiedenartigen Fensteroberflächen.
 Die Applikation, jetzt Klient genannt, möchte bei der Nutzung und
bei der Erzeugung der Objekte plattformunabhängig bleiben.
 flexible Nutzung der verschiedenen Plattformen
 konsistente Nutzung
 MS-Fenster und Motif-Fenster nicht in einer Applikation
 Erweiterbarkeit
 wenn ein neue Window-Oberfläche eingeführt wird

615
UML-Diagramm

<<uses>>
<<interface>> Fabrik Klient
createProductA() <<uses>>
createProductB()
<<interface>> ProductA

<<creates>>
ProductA1 ProductA2
KonkreteFabrik1 KonkreteFabrik2

createProductA() createProductA() <<interface>> ProductB


createProductB() createProductB()

<<creates>>
ProductB1 ProductB2
<<creates>>

616
Implementierungsaspekte

 Von konkreten Fabriken wird nur eine Instanz benötigt.


 Produkterzeugung
 AbstractFactory ist nur eine Schnittstelle für die Erzeugung
 Die Erzeugung der Produkte erfolgt in den eigentlichen Klassen, die
dann in den entsprechenden Fabriken aufgerufen wird.
 Erweiterbarkeit von Fabriken
 Parameter spezifiziert, welches Objekt tatsächlich erzeugt werden
soll.
 Die Erzeugungsfunktion liefert ein abstraktes Objekt zurück.
 Keine volle Nutzung der Funktionalität der Objekte, da deren
konkrete Klasse unbekannt ist.

617