Sie sind auf Seite 1von 179

Skript zur Vorlesung

Objektorientierte Programmierung

Teil 1: Programmieren in C++

Teil 2: Datenstrukturen und Algorithmen in C++

von

Erich Eich

HS Mannheim

Fakultät Informationstechnik

Stand 25.07.2014

Inhaltsverzeichnis

Teil 1

Programmieren in C++

1

1. Einführung

1

1.1 Grundbegriffe der objektorientierten Programmierung

1

1.2 Historie der objektorientierten Programmierung

3

1.3 Notation der Syntax

4

2. Spracherweiterungen gegenüber C

5

2.1 Kommentare

5

2.2 Der Datentyp bool

5

2.3 Strukturen und Aufzählungstypen

6

2.4 Variablendefinition

6

2.5 Modifikation mit const

6

2.6 Referenzen

7

2.7 Überladen von Funktionen

9

2.8 Default-Werte für Funktionsparameter

10

2.9 Inline-Funktionen

10

2.10 Der Bereichsoperator ::

11

2.11 Namensräume

11

2.12 Freispeicherverwaltung

14

2.13 Einfache Ein-/Ausgabe

15

2.14 Übungsaufgaben

17

3. Klassen und Objekte

18

3.1

Klassen

18

3.1.1 Klassendeklaration

18

3.1.2 Objekterzeugung und Komponentenzugriff

20

3.1.3 Elementfunktionen

20

3.1.4 Konstruktoren

22

3.1.5 Elementinitialisierungsliste

23

3.1.6 Destruktoren

24

3.2

Objekte

24

3.2.1 Globale, statische und automatische Objekte

25

3.2.2 Dynamische Objekte

26

3.3

Komposition von Objekten

27

3.3.1 Elementobjekte

27

3.3.2 Zeiger auf Objekte als Komponenten

29

3.3.3 Felder von Objekten

30

3.3.4 Dynamisch allokierte Felder von Objekten

31

3.4 Geschachtelte Klassen

33

 

3.5 Friends

34

3.6 Statische Komponenten

35

3.7 Übungsaufgaben

37

 

4. Vererbung

39

4.1 Abgeleitete Klassen

39

4.2 Zugriffskontrolle

43

4.3 Konvertierbarkeit von Zeigern

44

4.4 Virtuelle Funktionen

46

4.4.1 Spätes Binden

47

4.4.2 Abstrakte Klassen

49

4.4.3 Ein Praxisbeispiel

50

4.5.1

Mehrdeutigkeiten durch Namenskonflikte

54

 

4.5.2 Mehrdeutigkeiten durch wiederholtes Vererben

54

4.5.3 Virtuelle Basisklassen

56

4.6

Übungsaufgaben

58

5.

Überladen von Operatoren

60

5.1

Grundlagen

60

5.3

friend-Funktionen oder Elementfunktionen

65

5.4

Überladen des Index-Operators

66

5.5

Zuweisung und Initialisierung

67

5.5.1 Zuweisung

67

5.5.2 Initialisierung

68

5.6

Übungsaufgaben

70

6.

Templates

71

 

6.1 Funktions-Templates

71

6.2 Klassen-Templates

74

6.3 Übungsaufgaben

76

7.

Exceptions

77

 

7.1 Exception-Mechanismus

77

7.2 Exception Spezifikation

80

7.3 Stack-Abwicklung

81

7.4 Übungsaufgaben

83

8.

Die E/A-Bibliothek

84

8.1 Stream-Klassen

84

8.2 Standard-Ein/Ausgabe von eingebauten Typen

85

8.2.1 Ausgabe

85

8.2.2 Eingabe

86

8.3 Stream-Status

87

8.4 Standard-Ein-/Ausgabe von benutzerdefinierten Typen

88

8.4.1 Ausgabe

88

8.4.2 Eingabe

89

8.5

Formatierung

91

8.5.1 Formatierung durch Manipulation des Stream-Objekts

91

8.5.2 Formatierung mittels Manipulatoren

93

8.6

Dateien und Streams

97

8.6.1 Öffnen von Dateien

97

8.6.2 Schließen von Dateien

98

8.7

Übungsaufgaben

100

Teil 2

Datenstrukturen und Algorithmen in C++

101

9.

Datentypen und Datenstrukturen

101

9.1 Datentypen

101

9.2 Datenstrukturen

101

9.3 Standarddatenstrukturen

102

10. Sequenziell gespeicherte lineare Listen

104

 

10.1 Definition

104

10.2 Abstraktion

105

10.3 Implementierung

106

10.4 Suchen

109

10.4.1 Komplexität von Algorithmen

111

10.4.2 Komplexität der Suchalgorithmen

112

10.5. 1 Quicksort

113

10.6

Übungen

116

11.

Verkettet gespeicherte lineare Listen

118

11.1 Speicherung in einem Array

118

11.2 Speicherung in dynamischen Variablen

119

11.2.1 Abstraktion

119

11.2.2 Implementierung

120

11.3 Doppelt verkettete Listen

123

11.4 Übungsaufgaben

124

12.

Spezielle Listen

125

12.1

Stapel

125

12.1.1 Abstraktion

125

12.1.2 Implementierung

126

12.2

Schlangen

128

12.2.1 Abstraktion

128

12.2.2 Implementierung

129

12.3

Übungsaufgaben

131

13.

Bäume

132

13.1 Grundlagen

132

13.2 Suchbäume

133

13.2.1 Abstraktion

133

13.2.2 Implementierung

134

13.2 Traversieren von Binärbäumen

139

13.3 Übungsaufgaben

141

14.

Graphen

142

14.1

Grundlagen

142

14.2

Wegesuche

143

14.3

Abstraktion

144

14.4

Implementierung

144

14.4

Übungsaufgaben

148

15 Die Standard-Template Library (STL)

149

15.1 Konzept der STL

149

15.2 Container

151

15.3 Iteratoren

153

15.4 Algorithmen

155

15.5 Übungsaufgaben

160

16.

Datenstrukturen und Dateien

161

16.1 Persistenz und Serialisierung

161

16.2 Gestreute Speicherung (Hashverfahren)

163

16.2.1 Abstraktion

165

16.2.2 Implementierung

166

16.3

Übungsaufgaben

170

Literatur

171

Index

172

Teil 1

Programmieren in C++

1. Einführung

Die “objektorientierte Programmierung” (OOP) hat sich in den vergangenen Jahren zur do- minierenden Programmiertechnik entwickelt. Hierbei spielt die Programmiersprache C++ eine wesentliche Rolle. Objektorientierten Programmen wird ein besonderer Grad an Wohl- strukturiertheit und Erweiterbarkeit nachgesagt. Um diese Qualitätseigenschaften zu errei- chen, ist ein guter Programmentwurf eine unabdingbare Voraussetzung.

Die objektorientierte Programmierung bringt neue Denkweisen und Programmiertechniken mit sich, die sich zunächst in einer Reihe neuer Begriffe manifestieren. Im Folgenden werden die wesentlichen Grundbegriffe kurz erläutert werden. Daran schließt sich ein kurzer Über- blick über die Entwicklung der objektorientierten Programmierung an.

1.1 Grundbegriffe der objektorientierten Programmierung

Objekte und Klassen

Ein Objekt ist eine gekapselte Datenstruktur, welche sowohl Datenkomponenten (analog zu einer struct-Variablen in C) als auch Funktionen zur Bearbeitung der Datenkomponenten enthält. Diese Funktionen werden generell als Methoden bezeichnet.

Die Objekte eines objektorientierten Programms repräsentieren reale oder konzeptionelle Objekte aus dem Problembereich. So könnte ein logisches UND-Gatter durch vier Objekte modelliert werden. Drei gleichartige Objekte repräsentieren die beiden Eingänge und den Ausgang, ein weiteres Objekt repräsentiert die eigentliche Schaltung. Die Ein- und Ausgänge enthalten als Datenkomponente das jeweils anliegende Signal.

Eingang 1als Datenkomponente da s jeweils anliegende Signal. Eingang 2 UND- Schaltung Ausgang Abb. 1.1 Objekte eines

Eingang 2Datenkomponente da s jeweils anliegende Signal. Eingang 1 UND- Schaltung Ausgang Abb. 1.1 Objekte eines UND-Gatters

UND- Schaltung
UND-
Schaltung
Ausgang
Ausgang

Abb. 1.1 Objekte eines UND-Gatters

Die Ausführung eines objektorientierten Programms bewirkt den Austausch von Botschaften unter den Objekten. Der Empfang einer Botschaft bewirkt die Ausführung einer gleichnami- gen Methode. Hierdurch können die Datenkomponenten des Objekts verändert oder abgefragt werden. Auch der Benutzer des Programms kann Botschaften an Objekte senden.

Ein Schaltvorgang in dem UND-Gatter könnte z.B. die in Abb. 1.2 gezeigte Folge von Bot- schaften auslösen.

1

schalte holeSignal Benutzer 1 2 Eingang 1 UND- Ausgang Schaltung 4 Eingang 2 setzeSignal 3
schalte
holeSignal
Benutzer
1
2
Eingang 1
UND-
Ausgang
Schaltung
4
Eingang 2
setzeSignal
3

holeSignal

Abb. 1.2 Botschaftenaustausch anlässlich eines Schaltvorgangs

Eine Klasse ist die programmtechnische Beschreibung eines Objekts oder - anders ausge- drückt - die Definition eines neuen Datentyps. Sie enthält Angaben zu den Namen und den Typen der Datenkomponenten sowie den Programmcode der Methoden. Um dem Prinzip der Datenkapselung gerecht zu werden, können die Komponenten einer Klasse als privat bzw. öffentlich gekennzeichnet werden. Während private Komponenten nur in den Methoden der Klasse selbst zugreifbar sind, kann auf öffentliche Komponenten - meist Methoden - auch von außen zugegriffen werden.

Vererbung

Klassen können unter Bezugnahme auf eine bereits existierende Klasse definiert werden. In diesem Falle erbt die neu definierte Klasse alle Komponenten der bereits bestehenden Klasse. Die neu definierte Klasse wird von der besteheden Klasse abgeleitet. Der Vorteil besteht da- rin, dass die geerbten Komponenten nicht neu definiert werden müssen. Die bestehende Klas- se bezeichnet man als Basisklasse, die neu definierte Klasse wird als abgeleitete Klasse be- zeichnet. Graphisch wird eine Ableitungsbeziehung wie in Abb. 3.1 dargestellt.

Basisklasse

ungsbeziehung wie in Abb. 3.1 dargestellt. Basisklasse abgeleitete Klasse Abb. 1.3 Vererbungsbeziehung Die

abgeleitete Klasse

Abb. 1.3 Vererbungsbeziehung

Die abgeleitete Klasse kann um Datenkomponenten und/oder Methoden erweitert werden oder die Komponenten der Basisklasse können redefiniert werden. In diesem Falle spricht man von Spezialisierung.

2

Polymorphismus

Als Polymorphismus bezeichnet man die Eigenschaft von objektorientierten Programmier- sprachen, dass die Interpretation einer Botschaft mit dem Typ des empfangenden Objekts variieren kann. Da der Typ des Empfängerobjekts nicht in allen Fällen schon zur Überset- zungszeit feststeht, erfordert die Implementierung des Polymorphismus ein spezielles Binde- verfahren, welches erst zu Laufzeit stattfindet und als spätes Binden bezeichnet wird.

Polymorphismus ist die wesentliche Grundlage für eine flexible Erweiterbarkeit objektorien- tierter Programme. Sie erlaubt im bestimmten Fällen sogar die Erweiterung eines Programms ohne die Verfügbarkeit des Quellcodes

1.2 Historie der objektorientierten Programmierung

(1) Seinen Ursprung hat die objektorientierte Programmierung in der Sprache Simula 67, eine objektorientierte Erweiterung von Algol, welche vornehmlich zum Zwecke der Simulation eingesetzt wurde.

(2) Das heutige Verständnis der objektorientierten Programmierung wurde vornehmlich durch die Programmiersprache Smalltalk geprägt. In der Erfordernis für eine radikal an- dere Denkweise im Programmdesign sowie in der anfänglich schlechten Laufzeiteffizienz liegen die Gründe für die heutige geringe praktische Bedeutung von Smalltalk.

(3) Mit den objektorientierten Spracherweiterungen von C (Objective C und insbesondere C++) wurde die objektorientierten Programmierung einem größeren Anwenderkreis zu- gänglich. C++ wurde zu Beginn 80-er Jahre von Bjarne Stroustrup (AT&T) vorgestellt. Es handelt sich um eine echte Obermenge von C, was den Vorteil hat, dass keine grund- sätzlich neue Sprache zu erlernen ist. Unter der Zielsetzung, die Laufzeiteffizienz von C möglichst wenig zu beeinträchtigen, wurden im Vergleich zu Smalltalk Kompromisse bei den objektorientierten Konzepten eingegangen. Im Jahre 1998 wurde die Standardi- sierung durch ANSI abgeschlossen.

(4) Seit 2000 gewinnt die von der Fa. Sun entwickelte, objektorientierte Programmiersprache Java zunehmend an Bedeutung.

3

1.3 Notation der Syntax

Zur Beschreibung der Syntax wird die Backus-Naur-Notation (BNF) verwendet. Aus didak- tischen Gründen werden dabei jedoch an einigen Stellen bewusst Vereinfachungen vor- genommen. Für eine vollständige und korrekte Syntaxbeschreibung wird auf die Literatur verwiesen.

Verwendete Symbolik

::=

Fettschrift

Schrägschrift

wird definiert als Terminalsymbole Nichtterminalsymbole

wird definiert als Terminalsymbole Nichtterminalsymbole
wird definiert als Terminalsymbole Nichtterminalsymbole

{

|

{

{

{

} 1

} opt

} 0+

} 1+

Beispiele:

Auswahl eines alternativen Syntaxbestandteils

trennt alternative Syntaxbestandteile

optionales Syntaxbestandteil

0-malige oder mehrmalige Wiederholung

1-malige oder mehrmalige Wiederholung

if-anweisung ::= if ( ausdruck ) anweisung { else anweisung } opt var-definition ::= typ { var-name { = const-ausdruck } opt } 1+ ; typ ::= { char | int | long | float | double } 1

4

2. Spracherweiterungen gegenüber C

C++ enthält eine Reihe von Spracherweiterungen, welche nicht in unmittelbarem Zusam- menhang mit den objektorientierten Eigenschaften der Sprache stehen. Diese haben vorrangig das Ziel, Sicherheit und den Komfort der Programmierung zu erhöhen und wurden teilweise in den ANSI-Standard der Sprache C übernommen.

2.1 Kommentare

In C wird ein - sich möglicherweise über mehrere Zeilen erstreckender - Kommentar in die Kommentarzeichen /* und */ eingefasst. Diese Möglichkeit bleibt in C++ weiterhin bestehen. Das zusätzliche Kommentarzeichen // (doppelter Schrägstrich) leitet einen Kommentar ein, der sich bis zum Zeilenende erstreckt.

int x;

// Das ist eine Kommentarzeile.

2.2 Der Datentyp bool

C++ kennt den Datentyp bool, zur Repräsentation von der Wahrheitswerten. Variablen vom Typ bool können nur die Werte true und false annehmen.

bool ok = true; if (ok)

Die Vergleichsoperatoren <, <=, >, >=, == und != liefern ein Ergebnis vom Datentyp bool. Ferner lassen sich Werte vom Typ bool mittels der Operatoren && (UND), || (ODER) und ! (NICHT) in der bekannten Weise verknüpfen.

Für den Datentyp bool existieren implizite Typumwandlungsmöglichkeiten von und nach int (siehe Tabelle 2.1)

Tabelle 2.1 Umwandlung bool <-> int

bool ->

int

int

->

bool

false

0

0

false

true

1

ungleich 0

true

5

2.3

Strukturen und Aufzählungstypen

Für alle Typnamen inkl. struct, union und enum gibt es einen gemeinsamen Namensraum. Dies bedeutet, dass die Typnamen eindeutig sein müssen. Andererseits kann die Angabe der Schlüsselwörter struct, union und enum z.B. bei der Definition von Variablen entfallen. Damit entfällt die Notwendigkeit zur Verwendung von typedef.

struct Person { char *name; char *vorname;

}; Person anybody = { "Mustermann", "Otto" };

2.4 Variablendefinition

In C++ ist es nicht zwingend vorgeschrieben, Variablen am Anfang eines Blocks zu definie- ren. Sie können beliebig zwischen oder innerhalb von Anweisungen eingestreut werden. Sie gelten bis an das Ende des nächstinneren Blocks. Im Falle einer for-Anweisung ist ihre Gül- tigkeit also auf den Block begrenzt, der die for-Anweisung enthält.

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

2.5 Modifikation mit const

Mittels des Schlüsselworts const können Variablen als konstant definiert werden. Im Gegen- satz zu ANSI-C können const-Größen auch zur Dimensionierung von Vektoren verwandt werden.

const int LEN=100; int vek[LEN];

const-Größen können nur auf dem Wege der Initialisierung mit einem Wert versehen wer- den. Außerdem kann man die Adresse einer const-Größe nicht einem normalen Zeiger zuweisen, da sonst eine Modifikation auf indirektem Wege möglich wäre:

const int LEN = 100;

int *p = &LEN;

// Fehler

Die Zuweisung der Adresse an einen mit const modifizierten Zeiger ist dagegen möglich. Eine indirekte Modifikation ist in diesem Falle nicht möglich:

const int LEN = 100; const int *p = &LEN;

// ok

*p = 200;

// Fehler

6

Es sei an dieser Stelle angemerkt, dass die const-Modifikation lediglich vor einer Verände- rung des adressierten Datenobjekts schützt. Der Zeiger selbst kann verändert werden. Wollte man den Zeiger selbst vor Veränderung schützen, müßte er wie folgt definiert werden:

int a;

int * const p = &a;

p = NULL;

//Fehler

2.6 Referenzen

Eine Referenz in C++ ist ein alternativer Name für ein bestehendes Datenobjekt.

int i = 5; int &r = i;

r = 10;

// Integer-Datenobjekt mit Wert 5

//

// gleichen Speicherplatz.

//

r und i beziehen sich auf den

i (und r) haben den Wert 10.

Dies kann wie folgt mit Hilfe von Zeigern äquivalent realisiert werden:

int i = 5; int *r = &i;

*r = 10;

// Integer-Datenobjekt mit Wert 5

//

// gleichen Speicherplatz

//

*r und i beziehen sich auf den

i (und *r) haben den Wert 10.

Das Beispiel zeigt, dass eine Referenz als ein Zeiger verstanden werden kann, welcher auto- matisch dereferenziert wird.

Eine Referenz muss wie eine const-Größe immer initialisiert werden. Eine nachträgliche Änderung ist nicht möglich, da eine Wertzuweisung an eine Referenz nicht den Wert der Referenz ändert, sondern das referenzierte Datenobjekt.

Syntax: referenztyp ::= typ &

(alternativ: typ& )

Der Adressoperator & erhält eine erweiterte Bedeutung und wird in dem Zusammenhang als Referenzoperator bezeichnet.

7

Parameterübergabe und Wertrückgabe mittels Referenzen

In C werden Parameter grundsätzlich als Wertparameter übergeben. Eine Übergabe per Refe- renz muss mit Hilfe von Zeigern nachgebildet werden Adressparameter).

Beispiel 2.1a:

(Adressparameter)

#include <stdio.h> void swap(int *a, int *b);

int main()

{

int i = 3, j = 5;

printf("Vorher: i=%d, j=%d\n", i, j); swap(&i, &j); printf("Nachher: i=%d, j=%d\n", i, j); return 0;

}

void swap(int *a, int *b)

{

int temp;

temp = *a; *a = *b; *b = temp;

}

Referenzen gestatten eine einfachere Realisierung von Referenzparametern. Beispiel 2.1b enthält eine Reimplementierung des Programms in Beispiel 2.1a mittels Referenzen.

Beispiel 2.1b: (Referenzparameter)

#include <stdio.h> void swap(int &a, int &b);

int main()

{

int i = 3, j = 5;

printf("Vorher: i=%d, j=%d\n", i, j); swap(i, j); printf("Nachher: i=%d, j=%d\n", i, j); return 0;

}

void swap(int &a, int &b)

{

int temp;

temp = a;

a = b;

b = temp;

}

Bemerkungen - Die Initialisierung der Referenzparameter erfolgt bei der Argumentübergabe. Die Refe- renzparameter können auch hier als Synonyme für die korrespondierenden Argumente interpretiert werden.

8

Parameterübergabe mittels Referenzen findet in 2 Fällen Anwendung:

(1)

(2) Aus Gründen der Laufzeit- und Speicherplatzeffizienz soll ein größeres Datenobjekt

Eine Funktion liefert zwei oder mehr Werte an den Aufrufer zurück.

nicht per Wert übergeben werden.

Möchte man im letztgenannten Fall vermeiden, dass das übergebene Datenobjekt in der Funktion irrtümlicherweise verändert wird (Seiteneffekte), so kann die Parameterangabe zu- sätzlich mit dem Modifizierer const versehen werden:

void ausgabe(const BigStruct &big);

Auch im Hinblick auf das Verständnis der Funktionsweise einer Funktion ist die const- Modifikation eines als Referenz übergebenen Parameters sehr hilfreich, da auf diese Weise erkennbar ist, dass es sich um einen reinen Eingabeparameter handelt.

Der Wert einer Funktion kann ebenfalls vom Referenztyp sein. In diesem Falle kann der Funktionsaufruf auch auf der linken Seite einer Wertzuweisung auftauchen:

int& valAt(const int &vek, int i); // liefert Referenz auf i-te Komponente von vek

int v[10]; valAt(v, 0) = 10; // gleichbedeutend mit: v[0] = 10;

Eine Funktion darf keine Referenz auf ein lokal definiertes Datenobjekt zurück liefern.

2.7 Überladen von Funktionen

Grundsätzlich sollten verschiedene Funktionen verschiedene Namen haben. In gewissen Fäl- len jedoch werden Funktionen mit gleicher Aufgabe für unterschiedlichen Parametertypen benötigt. In diesem Fall ist es sinnvoll, diese Funktionsgleichheit auch durch gleiche Funkti- onsnamen zu verdeutlichen. In C++ sind gleichnamige Funktionen mit unterschiedlichen Pa- rametertypen erlaubt (Überladen von Funktionen):

long quadrat(long x); double quadrat(double x);

Beim Aufruf einer überladenen Funktion muss der Compiler anhand der Argumenttypen prü- fen, welche Funktion gemeint ist. Die "am besten passende" Funktion wird ausgewählt. Da- bei kommt der Reihe nach die folgende Rangfolge von Umwandlungsregeln zur Anwendung:

1. Exakte Übereinstimmung: keine Umwandlung.

2. Umwandlung von char und short nach int sowie von float nach double

3. Standard-Umwandlungen wie sie auch in C üblich sind (u.U. in Verbindung mit Informa- tionsverlust, z.B. int nach float oder long nach int).

Zum Aufruf kommt die Funktion, welche gemäß obiger Aufstellung die niederrangiste Um- wandlung erfordert. Werden zwei Funktionen mit gleichrangiger Umwandlung gefunden, liegt eine Zweideutigkeit vor, welche zu einem Übersetzungsfehler führt.

9

quadrat(1L);

quadrat(1.0f);

quadrat(1);

Aufruf von quadrat(long) Aufruf von quadrat(double) zweideutig, Fehler

Im ersten Fall liegt eine exakte Übereinstimmung vor, im zweiten Fall findet eine Um- wandlung von float nach double gemäß Regel 2 statt. Der dritte Aufruf ist zweideutig, da sowohl für quadrat(long) als auch für quadrat(double) eine Standardumwandlung statt- finden müßte.

Für das Auffinden der "am besten passenden" Funktion muss die Typisierung der überla- denen Funktionen bekannt sein. Aus diesem Grund sind in C++ Funktionsdeklarationen obligatorisch. C++ wird aus diesem Grunde als Sprache mit strenger Typprüfung bezeich- net.

2.8 Default-Werte für Funktionsparameter

Funktionsdefinitionen können Vorbesetzungen (Default-Werte) von Parametern enthalten. Diese werden bei fehlenden Argumenten im Funktionsaufruf verwendet.

int add(int a, int b, int c=0, int d=0)

{

 

return a + b + c + d;

}

Nach dem ersten vorbesetzten Parameter müssen alle folgenden Parameter vorbesetzt sein. Nicht erlaubt wäre: int add (int a=0, int b, int c, int d=0)

Im Aufruf dürfen nach dem ersten fehlenden aktuellen Parameter keine weiteren Parameter aufgeführt werden.

korrekte Aufrufe:

nicht korrekte Aufrufe:

add(1,2,3,4);

add(1);

add(1,2,3);

add(1,2,,4);

add(1,2);

2.9 Inline-Funktionen

Für kleine, häufig verwendete Funktionen werden in C häufig Makros verwandt. Diese sind jedoch fehleranfällig. In C++ können statt der Makros Inline-Funktionen verwendet werden.

inline int quadrat(int x) { return x*x; }

Analog zu Makros fällt beim Aufruf einer Inline-Funktionen kein Stack-Overhead an. Sie sind jedoch weniger fehleranfällig, da sie in Syntax und Semantik mit den üblichen Funktio- nen übereinstimmen und der Typprüfung unterliegen.

Syntax: inline funktionsdefinition

10

2.10

Der Bereichsoperator ::

In C verdeckt eine Variable in einem inneren Block eine gleichnamige externe Variable. Der Bereichsoperator ( scope resolution operator) :: von C++ ermöglicht den Zugriff auf ver- deckte externe (globale) Namen.

Beispiel 2.2:

#include <stdio.h>

int x = 0;

// externes x

int main() { int x;

// 1. lokales x, verdeckt externes x

x = 1;

::x = 2;

// Zuweisung an 1. lokales x // Zuweisung an externes x

{

int x;

// 2. lokales x

x

= 3;

// Zuweisung an 2. lokales x

::x = 4;

// Zuweisung externes x

}

x

=

6;

// Zuweisung an 1. lokales x

return 0;

}

Syntax: ::var-name

2.11 Namensräume

Umfangreichere Programme verwenden häufig mehrere Bibliotheken. Dabei kann es zu Na- menskonflikten kommen. Werden z.B. zwei Typen mit gleichem Namen definiert, so können diese Bibliotheken nicht gemeinsam eingesetzt werden.

Das Programm in Beispiel 2.3 verwendet zwei Bibliotheken, deren Schnittstellen durch die Header-Dateien lib1.h bzw. lib2.h definiert sind.

Beispiel 2.3:

// Datei lib1.h

typedef enum { FALSE, TRUE } Boolean; void printBool(Boolean bval);

// Datei lib2.h

typedef enum { FALSCH, WAHR } Boolean; void printBool(Boolean bval);

Der Namenskonflikt betrifft den Enumerationstyp Boolean und die Funktion printBool().

11

Deklaration von Namensräumen

C++ sieht zur Lösung des Problems so genannte Namensräume (name spaces) vor. Ein Pro- gramm kann in mehrere Namensräume aufgeteilt werden. Bezeichner müssen nur im jeweili- gen Namensraum eindeutig sein. Die Deklaration eines Namensraums geschieht über das Schlüsselwort namespace, gefolgt von einem Bezeichner für den Namensraum und einem Block mit den zum Namensraum gehörenden Vereinbarungen.

Syntax: namespace name block

In Beispiel 2.3 werden die beiden Bibliotheken in den Namensräumen lib1 und lib2 definiert.

Beispiel 2.3 (Forts.):

// Datei lib1.h

namespace lib1

{

typedef enum { FALSE, TRUE } Boolean; void printBool(Boolean bval);

}

Beispiel 2.3 (Forts.):

// Datei lib2.h

namespace lib2

{

typedef enum { FALSCH, WAHR } Boolean; void printBool(Boolean bval);

}

Natürlich müssen die Funktionen auch im jeweiligen Namensraum definiert werden. Hierzu wird der Namensraum erneut geöffnet.

Beispiel 2.3 (Forts.):

// Datei lib1.cpp

#include <stdio.h> #include "lib1.h"

namespace lib1

{

void printBool(Boolean bval)

{

if (bval == FALSE) printf("FALSE"); else printf("TRUE");

}

}

// namespace lib1

Bezeichner, welche außerhalb einer namespace-Deklaration vereinbart werden, gehören dem namenlosen Standard-Namensraum.

12

Zugriff

Innerhalb eines Namensraums können dort definierte Bezeichner ohne weitere Angaben an- gesprochen werden. Außerhalb des Namensraums ist ein qualifizierter Name erforderlich. Ein qualifizierter Name besteht aus der Angabe des Namensraums, gefolgt von dem Be- reichsoperator ::, gefolgt von dem eigentlichen Bezeichner.

Syntax: namespace::name

Die Funktion main() in Beispiel 2.3 ist im namenlosen Standard-Namensbereich definiert. Deshalb muss auf die Bestandteile der Bibliotheken mit qualifiziertem Namen zugegriffen werden.

Beispiel 2.3 (Forts.):

// Datei bsp_2_3a.cpp - Qualifizierte Namen

#include "lib1.h" #include "lib2.h"

int main() { lib1::Boolean ok; int i = 1, j = 2;

ok = (lib1::Boolean) (i < j);

lib1::printBool(ok);

}

using-Direktive

Mit Hilfe der using-Direktive kann ein Namensraum in den aktuellen Gültigkeitsbereich importiert werden. Hierdurch werden auch alle im importierten Namensraum definierten Be- zeichner ohne weitere Angaben gültig.

Syntax:

using namespace name ;

Beispiel 2.3 (Forts.):

// Datei bsp_2_3b.cpp - using-Direktive

#include "lib1.h" #include "lib2.h"

using namespace lib1;

int main()

{

Boolean ok; int i = 1, j = 2;

ok = (Boolean) (i < j); printBool(ok);

}

13

Bemerkungen zu Beispiel 2.3

- Bezeichner aus dem Namensraum lib2 müssen weiterhin qualifiziert angesprochen wer- den.

- Ein zusätzlicher Import von Namensraum lib2 würde erneut zu Namenskonflikten führen.

2.12 Freispeicherverwaltung

Die Operatoren new und delete ermöglichen die dynamische Reservierung bzw. Freigabe von Speicherplatz im Freispeicher.

int *p1 = new int; // Reservierung von Speicherplatz // und Initialisierung von p1

Im Erfolgsfall reserviert new Speicherplatz für einen als Argument angegebenen Typ und liefert einen entsprechend typisierten Zeiger zurück. Im Fehlerfall wird ein NULL-Zeiger zurückgeliefert. Mit dem Operator new kann eine Angabe zur Initialisierung des reservierten Bereichs angegeben werden:

int *p2 = new int(3); // zusätzlich Initialisierung // von *p2 mit Wert 3

Die Reservierung von Speicherplatz für ein Array geschieht mit dem Operator new[] unter Angabe der gewünschten Dimension.

char *str = new char[10]; // 10-elementigen char-Array

int (*mat)[4] = new int[2][4]; Arrays

// Zwei 4-elementige int-

- Der zurückgelieferte Zeiger zeigt auf das erste Array-Element.

- Eine Initialisierung ist in diesem Fall nicht möglich. Es findet auch keine automatische Initialisierung statt.

delete gibt den reservierten Speicherplatz zurück. Die Freigabe eines mittels new [] reser- vierten Speicherbereichs muss mit dem Operator delete[] frei gegeben werden.

delete p1; delete [] str;

Syntax:

new typ {( initialwert ) | [ dimension ] } 1 opt ; delete { [] } opt zeiger ;

14

2.13 Einfache Ein-/Ausgabe

C++ verfügt über eine E-/A-Bibliothek, welche auf dem Klassenkonzept basiert und so der objektorientierten Programmiermethodik besser entgegenkommt. Die Schnittstelle der Biblio- thek befindet sich in der Header-Datei iostream (ohne .h). Sie ist im Namensraum std defi- niert, der über eine using-Direktive importiert werden muss.

An dieser Stelle werden die Möglichkeiten der Bibliothek zunächst sehr vereinfacht und ein- geschränkt auf die Standard-Ein-/Ausgabe vorgestellt. Eine weiterführende Beschreibung befindet sich in Kapitel 8.

Jedes C++-Programm verfügt über die folgenden Objekte, welche Ein-/Ausgabeströme (Streams) repräsentieren

- cin für die Standardeingabe (von der Tastatur)

- cout für die Standardausgabe (auf den Bildschirm)

- cerr für die Fehlerausgabe (auf den Bildschirm).

Eingabe

Auf dem Eingabe-Stream cin ist der Eingabeoperator >> (vergl. Rechts-Shift) definiert.

int i; cin >> i;

Der >>-Operator führt die Eingabe durch und konvertiert diese entsprechend dem Typ der Variablen auf der rechten Seite. Führende “white-Spaces“ werden ignoriert. Als Ergebnis des Ausdrucks “cin >> i“ wird das Objekt cin zurückgeliefert. Deshalb kann der Ausdruck selbst auf der linken Seite des >>-Operators auftreten.

int i; double a; cin >> i >> a;

// (cin >> i) >> a;

- Die Bestandteile der Eingabe müssen dabei durch ein “whitespace“-Zeichen getrennt sein.

Ausgabe

Auf den Ausgabe-Streams cout und cerr ist der Ausgabeoperator << (vergl. Links-Shift) definiert:

int i; cout << i;

Hierbei wird die interne Darstellung des jeweiligen Datentyps in eine Folge von Zeichen konvertiert und auf dem Bildschirm ausgegeben. Auch im Falle des Ausgabeoperators << ist eine Verkettung möglich:

15

int i = 2; double a = 2.5; cout << i << ‘\n‘ << a;

Eingabe- und Ausgabeoperator akzeptieren als zweiten Operanden Variablen der Standardda- tentypen char, char*, void*, bool, short, int, long, float und double sowie die vorzeichen- losen Varianten der Ganzzahltypen.

Die Ein-/Ausgabe in C++ wird abschließend in einem Beispielprogramm demonstriert:

Beispiel 2.4: (Ein-/Ausgabe)

#include <iostream>

using namespace std;

int main()

{

int anz; double preis; char produkt[20];

cout << "Bezeichnung: "; cin >> produkt;

cout << "Einzelpreis (EUR): "; cin >> preis;

cout << "Anzahl :"; cin >> anz;

cout << anz << " " << produkt << anz * preis << " EUR\n";

<< " kosten "

return 0;

}

Das Programm berechnet im Dialog mit dem Benutzer den Gesamtpreises für die eingegebe- ne Menge eines bestimmten Produktes aus.



2.14 Übungsaufgaben

Aufgabe 2.1 (Referenzen)

Schreiben Sie eine Funktion, welche zwei int-Zeiger vertauscht. Verwenden Sie dabei Refe- renzparameter.

Aufgabe 2.2 (Referenzen)

Schreiben Sie eine C++-Funktion namens at(), welche wie folgt deklariert ist:

int& at(int* vec, unsigned len, unsigned i);

Die Funktion liefert eine Referenz auf das i-te Element des len langen int-Array vec zurück. Falls i einen ungültigen Index enthält, bricht das Programm mit der Fehlermeldung “index overflow” ab.

Testen Sie die Funktion mit folgendem Hauptprogramm:

#include <iostream> using namespace std;

int main()

{

const int LEN = 5 ; int vec[LEN] = { 0, 1, 2, 3, 4 };

at(vec,LEN,1) = 1001; cout << at(vec, LEN, 1) << '\n'; cout << at(vec, LEN, 5) << '\n'; return 0;

}

Aufgabe 2.3 (Allgemeine Fragen)

(a)

In einem Programm seien folgende Funktionen definiert. (1) int Add(int x, double y); (2) double Add(double x, int y); Welche Funktion wird bei dem Aufruf Add(10,5); ausgeführt?

(b)

Welche Vorteile haben Inline-Funktionen gegenüber normalen C-Funktionen?

(c)

Wann sollte man einen Referenzparameter mit dem Attribut const ausstatten?

(d)

Welche der folgenden Anweisungen ist bei der Deklaration von const int v[3] = {1, 2, 3}; zulässig?

(1) *v =3;

(2) (*v)++;

(3) int q=*v;



3. Klassen und Objekte

3.1 Klassen

Eine Klasse stellt, wie in Kapitel 1 bereits ausgeführt, eine Möglichkeit zur Formulierung eines anwendungsbezogenen Datentyps dar. Auch in C lassen sich mit Hilfe von Strukturty- pen anwendungsbezogene Datentypen formulieren. Ein Datentyp zur Modellierung von UND-Gattern kann in C etwa wie folgt definiert werden:

enum Signal { L, H };

struct UndGatter { enum Signal ein1; enum Signal ein2; enum Signal aus;

};

*/

/* Eingänge */

*/

/* zwei

/* Ausgang

Eine Funktion namens schalten() soll die Funktionsweise eines UND-Gatters simulieren:

void schalten(struct UndGatter *g)

{

g->aus = (enum Signal) (g->ein1 && g->ein2);

}

Unschön ist hierbei das Fehlen einer expliziten Verbindung zwischen dem Strukturtyp UndGatter und der auf diesem Datentyp operierenden Funktion schalten() sowie die fehlen- de Möglichkeit zur Datenkapselung. Beides wird durch das Klassenkonstrukt in C++ elimi- niert.

3.1.1 Klassendeklaration

Bei dem Klassenkonstrukt von C++ handelt es sich um eine Weiterentwicklung der Struktur- typen aus C.

Klassen können neben der Deklaration von Variablen auch Funktionsdeklarationen ent- halten. Die Variablen werden als Instanzvariablen, die Funktionen als Elementfunktio- nen (member functions) oder Methoden bezeichnet.

Die Komponenten einer Klasse (Instanzvariablen und Elementfunktionen) können als private oder öffentliche Komponenten vereinbart werden.

Privat vereinbarte Komponenten sind lediglich in den Elementfunktionen der Klasse zugreif- bar. Hierdurch kann insbesondere die interne Repräsentation eines Objekts nach außen zu verborgen werden (Information Hiding). Der Zugriff auf den Datentyp wird auf die öffent- lich deklarierten Funktionen begrenzt (Datenkapselung).



Beispiel 3.1: (Klassendeklaration)

class UndGatter { private:

Signal ein1; Signal ein2; Signal aus; void schalten(void); public:

// privater Teil

// oeffentlicher Teil

UndGatter(); UndGatter(Signal s1, Signal s2); int setEingangsSignal(unsigned nr, Signal sig);

Signal getAusgangsSignal(void) const;

};

Bemerkungen

- Die Klasse enthält neben den privaten Instanzvariablen ein1, ein2 und aus die öffentli- chen Zugriffsfunktionen setEingangsSignal() und getAusgangsSignal(). Die Element- funktion schalten() ist ebenfalls privat und kann damit nur in Elementfunktionen der Klasse UndGatter aufgerufen werden.

- Im Beispiel ist der unmittelbare Zugriff auf die Variablen der Klasse nicht möglich. Die Veränderung und Abfrage der Variablen erfolgt über die öffentlich zugänglichen Ele- mentfunktionen setEingangsSignal() und getAusgangsSignal(); d.h. die im privaten Teil der Klassendeklaration aufgeführten Variablen sind gekapselt.

- Falls die Deklaration einer Elementfunktion mit dem Zusatz const versehen ist, kann die Funktion keine Veränderungen an den Instanzvariablen vornehmen.

Allgemein gilt für Klassendeklarationen die folgende Syntax:

Syntax:

class class-name { { private : } opt member-liste public :

member-liste

};

Bemerkungen

- member-liste kann Typ- und Variablendeklarationen (Instanzvariablen) sowie die De- klaration bzw. Definitionen von Funktionen (Elementfunktionen) enthalten.

- Die Schlüsselworte private und public können grundsätzlich beliebig oft und in jeder Reihenfolge auftreten. Sie gelten jeweils bis zur nachfolgenden Angabe. In der Praxis beschränkt sich die Klassendeklaration auf einen privaten und einen öffentlichen Be- reich. Bei fehlender Zugriffsangabe sind die nachfolgenden Komponenten standard- mäßig private.

- In C++ sind Klassen- und Strukturdeklarationen weitestgehend äquivalent. D.h. auch Strukturdeklarationen können Elementfunktionen enthalten. Ferner erlauben sie die Unterscheidung zwischen privaten und öffentlichen Komponenten. Im Unterschied zu Klassen sind die Komponenten einer Struktur standardmäßig public. Meist wird je- doch das Klassenkonstrukt zur Definition von anwendungsbezogenen Datentypen vor- gezogen.



3.1.2 Objekterzeugung und Komponentenzugriff

Analog zu einer Strukturdeklaration in C kann eine Klasse in C++ zur Erzeugung eines Ob- jekts herangezogen werden. (Da der Objektbegriff auch in anderen Zusammenhängen sehr strapaziert wird, werden Objekte im vorliegenden Zusammenhang häufig auch als Klassen- objekte oder Instanzen einer Klasse bezeichnet.) Ein Objekt kann durch Definition einer Variablen vom Klassentyp oder dynamisch mit Hilfe des new-Operators erzeugt werden.

UndGatter gate;

UndGatter *dynGate; dynGate = new UndGatte;

Der Zugriff auf öffentliche Instanzvariablen und Elementfunktionen erfolgt über die aus C bekannten Selektionsoperatoren . bzw. ->:

gate.setEingangsSignal(1, H);

Signal sig; sig = dynGate->getAusgangsSignal();

3.1.3 Elementfunktionen

Elementfunktionen können innerhalb und außerhalb der Klassendeklaration definiert werden. Ist die Funktionsdefinition vollständig in der Klassendeklaration enthalten, handelt es sich in jedem Fall um eine Inline-Funktion. Soll eine Elementfunktion nicht als Inline-Funktion rea- lisiert werden, so muss sie außerhalb der Klassendeklaration definiert werden. Die Klassen- deklaration enthält in diesem Falle nur die Funktionsdeklaration (Prototyp). Natürlich können auch extern definierte Elementfunktionen ausdrücklich als Inline-Funktion definiert werden. In diesem Fall wird die Funktion meist in einer Header-Datei definiert sein.

Da verschiedene Klassen gleichnamige Elementfunktionen haben können, muss bei der (externen) Definition von Elementfunktionen der Bezug zur Klasse hergestellt werden. Getrennt durch den Bereichsoperator :: wird der Funktionsname mit dem Klassenname qualifiziert.

Beispiel 3.1 (Forts.): (Elementfunktionen)

inline void UndGatter::schalten(void)

{

aus = (Signal) (ein1 && ein2);

}

Der externen Definition von Elementfunktionen liegt damit folgende Syntax zugrunde:

Syntax: { inline } opt { typ } opt klassen-name::funktions-name ( { parameter-deklaration } opt ) block



Selbstreferenz über den Zeiger this

Innerhalb von Elementfunktionen kann auf alle Komponenten des “aktuellen Objekts” durch bloße Angabe des Komponentennamens zugegriffen werden. Mit aktuellem Objekt ist das Objekt gemeint, über das die Elementfunktion (unter Verwendung von . oder ->) aufgerufen wurde.

Beim Aufruf einer Elementfunktion wird - für den C++-Programmierer nicht sichtbar - ein konstanter Zeiger namens this auf das aktuelle Objekt übergeben.

Für die Klasse UndGatter ist this wie folgt definiert:

UndGatter *const this;

Innerhalb einer Elementfunktion erfolgt der Zugriff auf eine Komponente automatisch unter Verwendung des Zeigers this. In einer Elementfunktion der Klasse UndGatter sind daher die folgenden Ausdrücke äquivalent:

ein1

und

this->ein1

schalten()

und

this->schalten()

In Elementfunktionen einer Klasse X, welche mit dem Zusatz const versehen sind, hat der Zeiger this den Typ const X *const this. Hierdurch ist sowohl der Zeiger selbst, als auch das Objekt vor Veränderungen geschützt.

Der Zeiger this wird auch vom Programmierer in Anspruch genommen, wenn es darum geht, die Adresse des aktuellen Objekts zu bestimmen oder das aktuelle Objekt als Wert oder als Referenz im Funktionswert zurückzugeben.

Gültigkeit von Namen in Elementfunktionen

Innerhalb einer Elementfunktion schiebt sich der Gültigkeitsbereich der Klasse zwischen den globalen Gültigkeitsbereich und den lokalen, auf die Funktion begrenzten Gültigkeitsbereich. Hieraus resultiert die folgende Rangfolge bzgl. der Gültigkeit von Bezeichnern.

(1)

lokal definierte Bezeichner

(2)

innerhalb der Klasse definierte Bezeichner

(3)

Bezeichner mit Dateigültigkeit

Mit Hilfe des Bereichsoperators :: kann der Zugriff wie folgt modifiziert werden:

(1)

class-name::name

Zugriff auf Komponente der Klasse

(2)

::name

Zugriff auf Bezeichner mit Dateigültigkeit



Dies sei an nachfolgendem Beispiel verdeutlicht:

class X { int x; public:

int f();

};

int x;

int X::f() { int x;

x = 1; X::x = 1; ::x = 1;

}

3.1.4 Konstruktoren

// lokales x // Instanzvariable // globales x

Klassendeklarationen enthalten meist eine typlose Elementfunktion, welche namensgleich mit der Klasse ist. Es handelt sich um den Konstruktor.

Ein Konstruktor ist eine spezielle Elementfunktion, welche automatisch beim Erzeugen eines Objekts aufgerufen wird, und meist Initialisierungsaufgaben wahrnimmt.

Beispiel 3.1 (Forts.): (Standardkonstruktor)

UndGatter::UndGatter(void)

{

ein1 = L; ein2 = L; schalten();

// Initialisieren // der Eingänge // Bestimmen des Ausgangs

}

Bemerkungen

- Bei der Definition eines Konstruktors wird kein Ergebnistyp angegeben (auch nicht void). Eine Wertrückgabe ist ebenfalls nicht möglich.

- Konstruktoren können parameterlos oder parametiert sein. Häufig findet man in einer Klassendeklaration auch mehrere Konstruktoren mit unterschiedlicher Parametrierung (Überladung), so dass mehrere Alternativen zur Initialisierung eines Objekts existieren. Weil ein parameterloser Konstruktor lediglich feste Standardwerte für die Initialisierung verwendet, wird er als Standardkonstruktor bezeichnet.

- Es sei an dieser Stelle bemerkt, dass es nicht Aufgabe eines Konstruktors ist, Speicherplatz für das Objekt zu reservieren. Diese Aufgabe übernimmt der Compiler bzw. - bei dyna- misch erzeugten Objekten - der new-Operator.

Beispiel 3.1 enthält einen zweiten Konstruktor, welcher als Parameter die Initialwerte für die Eingänge als Parameter enthält.



Beispiel 3.1 (Forts.): (Überladener Konstruktor)

UndGatter::UndGatter(Signal s1, Signal s2 )

{

 

ein1 = s1; ein2 = s2; schalten();

// Initialisieren // der Eingänge // Bestimmen des Ausgangs

}

Falls ein parametrierter Kontruktor zur Initalisierung heran gezogen werden soll, muss die Objektdefintion eine Initialiserungsliste für die Konstruktorparameter enthalten:

UndGatter gate(L,L);

UndGatter *dynGate; dynGate = new UndGatte(H,H);

Enthält eine Klassendeklaration keinen Konstruktor, so generiert der Compiler automa- tisch ein parameterloser Standardkonstruktor. Die Instanzvariablen des Objekts enthalten dann unbestimmte Werte. Auf die Bedeutung eines solchen Konstruktors wird später ein- gegangen.

3.1.5 Elementinitialisierungsliste

Wie bereits oben erwähnt, besteht die Aufgabe der Konstruktoren meist darin, die Objekte zu initialisieren. Mit der Elementinitialisierungsliste sieht C++ eine zweite, vereinfachte Mög- lichkeit zur Initialisierung der Instanzvariablen des Objekts vor. Das folgende Beispiel zeigt eine weitere Alternative für einen UndGatter-Konstruktor:

Beispiel 3.1 (Forts.): (Konstruktor mit Elementinitialisierungsliste)

UndGatter::UndGatter(Signal s1, Signal s2) : ein1(s1), ein2(s2)

{

schalten();

// Bestimmen des Ausgangs

}

Bemerkungen

- Die Elementinitialisierungsliste wird durch einen Doppelpunkt von der Parameterliste in der Konstruktordefinition getrennt. Sie besteht aus einer Liste von Bezeichnern für In- stanzvariablen, gefolgt von einem in Klammern angegebenen Initialwert.

- Die Reihenfolge der Listeneinträge hat für die Reihenfolge der Initialisierung keine Bedeu- tung. Entscheidend hierfür ist die Reihenfolge der Deklaration der Instanzvariablen.

Eine besondere Bedeutung hat die Elementinitialisierungsliste für Initialisierung von In- stanzvariablen vom Referenztyp. Diese können nur über einen Eintrag in der Elementinitiali- sierungsliste mit Werten versorgt werden:



class Referenz { int &ref; public:

Referenz(int &r); //

};

Referenz(int &r) : ref(r)

{

}

3.1.6 Destruktoren

In einigen Fällen sind auch beim Löschen eines Objekts bestimmte Aktionen erforderlich. Dies ist insbesondere dann der Fall, wenn das Objekt einen Zeiger auf einen dynamischen Speicherbereich enthält, der mit dem Löschen des Objekts freigegeben werden muss. Zu die- sem Zweck sieht C++ den Destruktor vor.

Ein Destruktor ist eine spezielle Funktion, welche automatisch beim Löschen eines Ob- jekts aufgerufen wird.

Analog zum Konstruktor wird bei Fehlen eines Destruktors ebenfalls ein Standardestruk- tor erzeugt.

Beim Löschen eines Objekts der Klasse UndGatter sind keine Aktionen der oben beschrie- benen Art erforderlich. Daher ist die Definition eines Destruktors in diesem Fall nicht erfor- derlich. Unabhängig davon würde ein Destruktor für die Klasse UndGatter wie folgt ausse- hen:

Beispiel 3.1 (Forts.):

(Destruktor)

UndGatter::~UndGatter(void)

{

}

Bemerkungen

- Zur Benennung des Destruktors wird dem Klassennamen der "Not"-Operator ~ vorange- stellt.

- Destruktoren sind typ- und parameterlos.

3.2 Objekte

Wie andere Datenobjekte in der Programmiersprache C können Klassenobjekte unterschied- lichen Gültigkeitsbereich und Lebensdauer haben.



3.2.1 Globale, statische und automatische Objekte

Objekte können

- extern

- als statische Objekte innerhalb einer Funktion

- als lokale (automatische) Objekte oder als Parameter

deklariert sein. Ihre Gültigkeit und Lebensdauer ist analog zu der anderer Datenobjekte in C.

Bei der Definition eines Objekts übernimmt der Klassenname wie bereits in 3.1.2 darge- stellt, die Rolle der Typangabe.

Im Falle von Standardkonstruktoren reicht die Angabe eines Objektnamens.

Verfügt die Klasse über einen parametrierten Konstruktor, muss bei der Objektdefinition eine in Klammern eingefasste Initialisierungsliste angegeben werden. Diese enthält die Argumente für den Aufruf des Konstruktors:

UndGatter gate(L,L);

Allgemein liegt der Definition eines Klassenobjekts die folgende Syntax zugrunde:

Syntax: klassen-name objekt-name { ( ausdruck {,ausdruck } 0+ )} opt ;

Falls der Konstruktor über nur einen Parameter verfügt, ist auch die aus C bekannte Syntax zur Initialisierung einer Variablen zulässig:

Syntax: klassen-name objekt-name = ausdruck;

Konstruktoren und Destruktoren werden, wie bereits oben erwähnt, bei der Erzeugung bzw. beim Löschen eines Objekts aufgerufen.

Falls es sich um ein globales Objekt (extern vereinbart) handelt, wird der Konstruktor einmalig zum Programmstart und der Destruktor einmal zum Programmende aufgerufen. Ein C++-Programmierer hat somit die Möglichkeit, Code zu formulieren, welcher vor bzw. nach der Funktion main() zur Ausführung kommt.

Für statische Objekte, welche innerhalb einer Funktion vereinbart wurden, wird der Kon- struktor einmalig beim erstmaligen Aufruf der Funktion aufgerufen. Der Destruktor wird - wie bei externen Objekten - zum Programmende durchlaufen.

Bei automatischen Objekten wird der Konstruktor bei jedem Aufruf der Funktion (d.h. bei jeder Anlage des Objekts auf dem Stack) aufgerufen. Beim Verlassen der Funktion wird der zugehörige Destruktor aufgerufen. Konstruktoren und Destruktoren für derartig ver- einbarte Objekte sollten deshalb kurze Laufzeit haben und ggf. als Inline-Elementfunktion definiert sein.

Die Verwendung von automatischen Objekten wird anhand einer einfachen Anwendung zu Beispiel 3.1 dargestellt.



Beispiel 3.1: (automatische Objekte)

// Datei bsp_b31.cpp // Anwendung der Klasse UndGatter

#include <iostream> #include "gate_b31.h"

using namespace std;

int main () { UndGatter gate;

gate.setEingangsSignal(1,H);

gate.setEingangsSignal(2,H);

cout << "Ausgang: " << (gate.getAusgangsSignal() == L ? 'L' : 'H') << '\n'; return 0;

}

Bemerkungen zur Programmstruktur

- Um eine möglichst universelle Nutzung zu erreichen, wird eine Klasse oder eine Menge von logisch zusammenhängenden Klassen in einem eigenen Programm-Modul, bestehend aus einer Header-Datei (mit Endung .h) und einer gleichnamigen Implementierungsdatei (mit Endung .cpp) definiert.

- Die Header-Datei enthält die Klassendeklaration und die Inline-Elementfunktionen, die Implementierungsdatei enthält die Definition der verbleibenden Nicht-Inline-Funktionen.

- Die Anwendung der Klasse mit der main()-Funktion wird in einer eigenen Datei (mit En- dung .cpp) spezifiziert.

3.2.2 Dynamische Objekte

Im Gegensatz zu Objekten, deren Gültigkeit und Lebensdauer durch die Art der Definition festgelegt ist, werden dynamische Objekte explizit erzeugt und gelöscht.

Dynamische Objekte werden mit Hilfe des new-Operators erzeugt. Hierdurch wird Speicher- platz zur Ablage des Objekts im Freispeicher reserviert. new liefert als Rückgabewert einen Zeiger auf das neu erzeugte Objekt.

UndGatter *gate; gate = new UndGatter; // Erzeugen eines Objekts

Die Anwendung des Operators new auf einen Klassentyp bewirkt einen impliziten Aufruf des Konstruktors der Klasse.

Bei einem parametrierten Konstruktor kann eine Initialisierungsliste angefügt werden.

gate = new UndGatter(L,L);



Das Löschen eines Objekts (Freigabe des Speichers) erfolgt mittels des delete-Operators un- ter Angabe des Zeigers auf das Objekt. Hierbei wird der zugehörige Destruktor aufgerufen:

delete gate;

3.3 Komposition von Objekten

In konkreten Anwendungen stellen Objekte vielfach sehr komplexe Datenstrukturen dar. Ein Objekt kann dabei selbst Instanzvariablen vom Klassentyp oder Zeiger auf Objekte vom Klassentyp enthalten. Diesen Sachverhalt bezeichnet man als Komposition oder Aggregation von Objekten.

3.3.1 Elementobjekte

Enthält eine Klasse eine Instanzvariable vom Klassentyp, so spricht man von einem Ele- mentobjekt.

Als Beispiel sollen die Ein- und Ausgänge eines UND-Gatters als Elementobjekte von Und- Gatter-Objekten modelliert werden. Hierzu ist eine eigenständige Klasse für Ein- bzw. Aus- gänge erforderlich. Diese wird im Folgenden allgemein als Konnektor bezeichnet.

Beispiel 3.2: (Elementobjekte)

enum Signal { L, H };

enum KonTyp

{ E, A };

// E = Eingang, A = Ausgang

class Konnektor { KonTyp typ; Signal sig; public:

Konnektor (KonTyp t = E); void setSignal(Signal s); Signal getSignal(void);

};

In der Klasse UndGatter können nun die Ein- und Ausgänge als Objekte der Klasse Konnektor definiert werden.

Beispiel 3.2 (Forts.): (siehe Beiblatt 3.2)

class UndGatter { Konnektor ein1; Konnektor ein2; Konnektor aus; void schalten(void); public:

UndGatter(void); int setEingangsSignal(unsigned nr, Signal sig); Signal getAusgangsSignal(void);

};



Die privaten Bestandteile der Konnektor-Objekte bleiben gekapselt; d.h. die Element- funktionen der Klasse UndGatter haben keinen Zugriff.

Der Speicherbereich des umschließenden Objekts enthält die Speicherbereiche der Ele- mentobjekte (siehe Abb. 3.1) Deshalb werden die Elementobjekte automatisch zusammen mit dem umfassenden Objekt erzeugt und initialisiert. Die Konstruktoren der Elementob- jekte werden automatisch vom Konstruktor des umfassenden Objekts angestoßen. D.h. der Code der Konstruktoren der Elementobjekte wird vor dem Code des Konstruktors des um- fassenden Objekts ausgeführt. Die Reihenfolge ist bestimmt durch die Reihenfolge ihrer Deklaration in der Klasse des umfassenden Objekts.

ein1 ein2 aus
ein1
ein2
aus

Speicherbereich des UndGatter-Objekts Speicherbereich des Konnektor-Objekts ein1

Speicherbereich des Konnektor-Objekts ein2

Speicherbereich des Konnektor-Objekts aus

Abb. 3.1: Speicherabbild eines UndGatter-Objekts mit Elementobjekten

Falls die umschließende Klasse über keinen Konstruktor verfügt, ruft ihr vom Compiler generierter Standardkonstruktor die Konstruktoren der Elementobjekte auf.

Falls die Konstruktoren der member-Objekte Argumente erwarten, können diese in der Elementinitialisierungsliste des Konstruktors der umfassenden Klasse angegeben werden.

Beispiel 3.2 (Forts.): (Elementinitialisierungsliste)

UndGatter::UndGatter(void) : ein1(E), ein2(E), aus(A)

{

}

Bemerkungen

- Die Einträge in der Elementinitialisierungsliste enthalten die Namen der Elementobjekte zusammen mit der Parameterversorgung der Konstruktoren.

Die Definition eines UndGatter-Objekts bewirkt die folgende Ausführungsreihenfolge von Konstruktoren:

1. Konnektor(E)

2. Konnektor(E)

3. Konnektor(A)

4. UndGatter()

- Konstruktion von ein1

- Konstruktion von ein2

- Konstruktion von aus

- Konstruktion des UndGatter-Objekts



Bei der Versorgung der Konstruktoren mit Argumenten kann ganz oder teilweise auf Default- Parameter zurückgegriffen werden. Der folgende UndGatter-Konstruktor ist daher gleich- wertig zu dem oben aufgeführten.

Beispiel 3.2 (Forts.):

UndGatter::UndGatter(void) : aus(A)

{

}

Mit dem Löschen eines Objekts werden dessen Elementobjekte ebenfalls gelöscht.

Die Destruktion der Elementobjekte erfolgt nach der Destruktion des umfassenden Objekts in der umgekehrten Reihenfolge ihrer Erzeugung.

Falls die umschließende Klasse über keinen Destruktor verfügt, übernimmt der Standard- destruktor die Aktivierung der Destruktoren der Elementobjekte.

3.3.2 Zeiger auf Objekte als Komponenten

Als eine Alternative zu den Elementobjekten kann eine Klasse Zeiger auf Klassenobjekte enthalten.

Beispiel 3.3: (Zeiger auf Objekte als Komponenten)

class UndGatter { Konnektor *ein1; Konnektor *ein2; Konnektor *aus; void schalten(void); public:

UndGatter(void); ~UndGatter(void); int setEingangsSignal(unsigned nr, Signal sig); Signal getAusgangsSignal(void);

};

Bemerkungen

- Abweichend zu Beispiel 3.2 sind Eingänge und der Ausgang hier durch Zeiger auf Kon- nektor-Objekte repräsentiert.

Speicherbereich des UndGatter-Objekts Speicherbereich des Konnektor-Objekts ein1 ein1 ein2 Speicherbereich des
Speicherbereich des UndGatter-Objekts
Speicherbereich des Konnektor-Objekts ein1
ein1
ein2
Speicherbereich des Konnektor-Objekts ein2
aus
Speicherbereich des Konnektor-Objekts aus

Abb. 3.2: Speicherabbild eines UndGatter-Objekts mit Zeigern auf Konnektor-Objekte



- Da es sich bei den über Zeiger referenzierten Objekten speicherplatzmäßig um separate, eigenständige Konnektor-Objekte im Freispeicher handelt (siehe Abb. 3.2), findet in diesem Fall keine automatische Erzeugung und Initialisierung statt. Diese muss explizit im Konstruktor der umfassenden Klasse vorgenommen werden.

Beispiel 3.3 (Forts.): (Explizite Konstruktion der member-Objekte)

UndGatter::UndGatter(void)

{

ein1 = new Konnektor(E); ein2 = new Konnektor(E); aus = new Konnektor(A);

}

Mit dem Löschen des umfassenden Objekts müssen über Zeiger referenzierte Objekte eben- falls gelöscht werden. Dieses muss - wie auch die Erzeugung - explizit erfolgen. Zu diesem Zweck ist ein Destruktor erforderlich, welcher das Löschen der Konnektor-Objekte über- nimmt.

Beispiel: 3.3 (Forts.): (explizite Destruktion der member-Objekte)

UndGatter::~UndGatter(void)

{

 

delete ein1;

// explizites Löschen

delete ein2;

// der über Zeiger

delete aus;

// referenzierten Objekte

}

3.3.3 Felder von Objekten

Objekte können auch in Form von Feldkomponenten auftreten. Das folgende Beispiel defi- niert ein 2-elementiges Feld von Konnektor-Objekten:

Konnektor ein[2]; // Feld von 2 Gattereingängen

Für jedes Klassenobjekt des Felds wird dessen Konstruktor aufgerufen.

Eine Parameterübergabe an den Konstruktor ist in diesem Falle nicht möglich. D.h., die Klasse muss über einen parmeterlosen Standardkonstruktor verfügen.

Das Beispiel 3.2 soll dahingehend geändert werden, dass die Eingänge eines UND-Gatters als zwei-elementiges Feld von Konnektor-Objekten deklariert werden (siehe auch Abb. 3.3).

Beispiel 3.4: (Felder von Objekten als Komponenten)

class UndGatter { Konnektor ein[2]; Konnektor aus; public:

// 2 Eingänge // Ausgang

UndGatter(void);

};

//



ein[0] ein[1] aus
ein[0]
ein[1]
aus

Speicherbereich des UndGatter-Objekts

Speicherbereich des Felds ein von Konnektor-Objekten

Speicherbereich des Konnektor-Objekts aus

Abb. 3.3: Speicherabbild eines UndGatter-Objekts mit einem Feld von Konnektor- Objekten

In diesem Beispiel wird ein parameterloser Standardkonstruktor zur Initialisierung der Objek- te des Felds ein benötigt. Zudem wird ein parametrierter Konstruktor zur Initialisierung des Objekts aus benötigt. Erreicht wird dies durch Überladen des Konstruktors.

Beispiel 3.4 (Forts.): (Felder von Objekten als Komponenten)

class Konnektor { KonTyp typ; Signal sig; public:

};

Konnektor(void) Konnektor (KonTyp t) //

{ typ = E; sig = L; } { typ = t; sig = L; }

Bemerkungen

- Der parametrierte Konstruktor darf in diesem Falle aus Gründen der Eindeutigkeit keinen Default-Wert für den Parameter t enthalten.

- Der Konstruktor der Klasse UndGatter benötigt in der Elementinitialisierungsliste ledig- lich einen Eintrag für den Ausgang aus.

Beispiel 3.4 (Forts.): (Felder von Objekten als Komponenten)

UndGatter::UndGatter(void) : aus(A)

{

}

Beim Löschen eines Felds von Klassenobjekten wird automatisch für jede Feldkomponen- te der Destruktor bzw. Standarddestruktor der Klasse aufgerufen.

3.3.4 Dynamisch allokierte Felder von Objekten

Die Klasse UndGatter soll nun dahingehend geändert werden, dass eine variable Anzahl von Eingängen möglich ist. Hierzu müssen die Konnektor-Objekte in einem variabel langen Feld angelegt werden. Da dieses dynamisch allokiert werden muss, enthält die UndGatter-Klasse - ähnlich wie in 3.3.2 - Zeiger auf den Vektor mit Gattereingängen bzw. auf das Objekt für den Gatterausgang (siehe auch Abb. 3.4).



Beispiel 3.5: (Dynamische Felder von Objekten)

class UndGatter { unsigned anzEin; Konnektor *ein; Konnektor *aus;

public:

// Anz. der Eingänge // Feld von Eingängen // Ausgang

UndGatter(unsigned anz = 2); // standardmäßig 2 Eingänge ~UndGatter(void); //

};

Bemerkungen

- Die Klasse verfügt über eine Instanzvariable anzEin, welche die Anzahl der Eingänge des Objekts angibt.

 
Speicherbereich d es UndGatter -O bjek ts

Speicherbereich des UndGatter-Objekts

     

anzEin

 
anzEin   Speicherbereich des Felds ein mit Konnektor -O b jekten
anzEin   Speicherbereich des Felds ein mit Konnektor -O b jekten
anzEin   Speicherbereich des Felds ein mit Konnektor -O b jekten

Speicherbereich des Felds ein mit Konnektor-Objekten

ein

ein    
   
Speicherbereich des Konnektor -Objekts aus
Speicherbereich des Konnektor -Objekts aus

Speicherbereich des Konnektor-Objekts aus

aus

 

Abb. 3.4: Speicherabbild eines UndGatter-Objekts mit Zeiger auf ein dynamisch allokiertes Feld von Konnektor-Objekten

Der Konstruktor erzeugt eine variable Anzahl von Konnektor-Objekten für die Eingänge sowie ein Konnektor-Objekt für den Ausgang.

Beispiel 3.5 (Forts.): (Dynamische Felder von Objekten)

UndGatter::UndGatter(unsigned anz)

{

}

anzEin = anz; ein = new Konnektor [anzEin]; aus = new Konnektor(A);

Auch bei der Erzeugung eines Felds von Objekten mittels new[] wird implizit für jede Feldkomponente ein parameterloser Standardkonstruktor aufgerufen.

Beim Löschen eines Felds von Objekten mittels delete[] wird für jede Feldkomponente der Destruktor aufgerufen.

Beispiel 3.5(Forts.): (Dynamische Felder von Objekten)

UndGatter::~UndGatter(void)

{

 

delete[] ein;

delete aus;

}



3.4 Geschachtelte Klassen

Klassendeklarationen können geschachtelt werden. Dies soll an einer Variante von Beispiel 3.2 gezeigt werden:

Beispiel 3.6: (Geschachtelte Klassen)

enum Signal { L, H };

class UndGatter {

enum KonTyp

{ E, A };

// E = Eingang, A = Ausgang

// Deklaration der inneren Klasse class Konnektor { KonTyp typ; Signal sig; public:

Konnektor (KonTyp t = E); void setSignal(Signal s); Signal getSignal(void);

};

// Instanzvariablen Konnektor ein1; Konnektor ein2; Konnektor aus; void schalten(void); public:

UndGatter(void); int setEingangsSignal(unsigned nr, Signal sig); Signal getAusgangsSignal(void);

};

Bemerkungen:

- Die gegenüber Beispiel 3.2 unveränderte Klasse Konnektor ist im privaten Teil der Klas- se UndGatter deklariert. Dies bewirkt, dass die Klasse außerhalb des Gültigkeitsbereichs von UndGatter nicht sichtbar ist.

Bei der Definition der Elementfunktionen für die Klasse Konnektor ist eine zweistufige Qualifizierung notwendig:

Beispiel 3.6 (Forts.): (Geschachtelte Klassen)

inline void UndGatter::Konnektor::setSignal(Signal s)

{

sig = s;

}



3.5 Friends

In vielen Fällen ist eine Klasse logisch eng verzahnt mit einer anderen Klasse. Ein Beispiel hierfür bilden die Klassen UndGatter und Konnektor. Konnektor-Objekte werden immer in Verbindung mit Objekten der Klasse UndGatter verwendet. Insofern ist es aus Effizienz- gesichtspunkten denkbar, den Elementfunktionen der Klasse UndGatter direkten Zugriff zu privaten Komponenten der Klasse Konnektor zu gewähren. Dieses kann mit Hilfe von friend-Deklarationen erreicht werden.

Eine Funktion, welche in einer Klasse als

ist, besitzt Zu-

griff zu privaten Komponenten der jeweiligen Objekte, ist aber selbst keine Komponente der Klasse.

friend-Funktionen deklariert

Die Klasse Konnektor aus Beispiel 3.3 wird derart modifiziert, dass die Elementfunktion UndGatter::schalten() als friend definiert wird.

Beispiel 3.7: (Friend-Funktionen)

class Konnektor { // friend void UndGatter::schalten(void); };

Die Elementfunktion UndGatter::schalten() kann nun wie folgt definiert werden:

Beispiel 3.7 (Forts.): (Friend-Funktionen)

void UndGatter::schalten(void)

{

aus->sig = (Signal) (ein1->sig && ein2->sig);

}

Übersetzungstechnisch stellt dies ein Problem dar, weil die Klasse UndGatter den Typ Konnektor benutzt. Die Klasse Konnektor ihrerseits verwendet den Typ UndGatter. Bei einer derart zyklischen Verwendung von Typnamen ist die Vorabdeklaration eines Typna- mens erforderlich.

Beispiel 3.7 (Forts.): (Friend-Funktionen)

enum Signal { L, H };

class Konnektor;

class UndGatter { Konnektor *ein1; Konnektor *ein2; Konnektor *aus; public:

};

//

// Vorabdeklaration



Unter anderem wegen dieser Übersetzungsproblematik wird vielfach eine ganze Klasse als friend deklariert. In diesem Falle können alle Elementfunktionen der als friend deklarierten Klasse auf private Elemente zugreifen.

Beispiel 3.7 (Forts.): (Friend-Klassen)

class Konnektor { // public:

// friend class UndGatter; // friend-Klasse

};

Auch eine beliebige C-Funktion kann als friend einer Klasse definiert werden. friend- Deklarationen sollten aber nur dort eingesetzt werden, wo der Vorteil des einfacheren Zu- griffs den Verlust der Datenkapselung rechtfertigt.

3.6 Statische Komponenten

Eine als static deklarierte Datenkomponente wird von allen Objekten der Klasse gemeinsam benutzt. Die Zugriffsrestriktionen entsprechen denen von nicht-statischen Komponenten; d.h. auch eine als static deklarierte Komponente ist "von außen" nur zugreifbar, wenn sie als public deklariert ist.

Die Klasse Konnektor aus Beispiel 3.2 soll um einen Zähler erweitert werden, welcher über die Anzahl der erzeugten Objekte Buch führt.

Beispiel 3.8: (Statische Komponenten)

class Konnektor { KonTyp typ; Signal sig; static unsignd anz; public:

Konnektor (KonTyp t = E); ~Konnektor(); void setSignal(Signal s); Signal getSignal(void); static unsigned getAnz(void);

};

Mit der Deklaration einer Datenkomponente als static wird noch kein Speicherplatz reser- viert. Hierzu ist zusätzlich eine externe Definition der Variablen notwendig. Das Schlüs- selwort static darf hierbei nicht erneut angegeben werden, da dieses mit der in C üblichen Bedeutung von static kollidieren würde.

Beispiel 3.8: (Forts.)

unsigned Konnektor::anz = 0;



Mit jeder Erzeugung eines Objekts von Typ Konnektor wird der Zähler Konnektor::anz im Konstruktor inkrementiert. Im Destruktor wird Konnektor::anz dekrementiert. Innerhalb der Klasse können statische Komponenten durch bloße Angabe ihres Namens angesprochen wer- den.

Beispiel 3.8: (Forts.)

Konnektor::Konnektor (KonTyp t)

{

typ = t; sig = L; anz ++;

}

Konnektor::~Konnektor()

{

anz--;

}

Analog zu statischen Datenkomponenten können Elementfunktionen, welche als static dekla- riert sind, nur in Verbindung mit dem Klassennamen, nicht über ein Objekt, aufgerufen wer- den. Da solche Funktionen nicht über einen this-Zeiger verfügen, haben sie lediglich Zugriff zu static-Komponenten.

Im Beispiel dient die statische Elementfunktion Konnektor::getAnz() als Zugriffsfunktion auf die statische Datenkomponente Konnektor::anz.

Beispiel 3.8: (Forts.)

unsigned Konnektor::getAnz(void)

{

return anz;

}

Außerhalb der Klasse erforderte der Zugriff auf eine statische Komponente oder der Aufruf einer statischen Elementfunktion einen qualifizierten Namen.

Beispiel 3.8: (Forts.)

#include <iostream> #include "gate_b34.h"

using namespace std;

// Anwendung der Klasse UndGatter

int main ()

{

UndGatter gate1, gate2;

cout << "Es wurden " << Konnektor::getAnz() << " Konnektoren erzeugt\n"; return 0;

}



3.7 Übungsaufgaben

Aufgabe 3.1

Entwickeln Sie eine Klasse namens IntVector, welche einen int-Array (Typ int*) kapselt. Die Klasse verfügt über

- einen Konstruktor, welcher als Parameter die Länge (Typ unsigned) des dynamisch anzu- legenden int-Array angibt,

- eine Elementfunktion size(), mit der sich die Länge der Liste abfragen lässt.

- eine Elementfunktion at(), welche eine Referenz auf das i-te Element des int-Array zurück liefert, falls i ein gültiger Index ist. Bei einem ungültigen Index bricht die Elementfunktion das Programm mit der Fehlermeldung “index overflow” ab.

- eine Ausgabefunktion out(), welche den gekapselten int-Array zeilenweise ausgibt. Entscheiden Sie selbst, ob die Klasse einen Destruktor benötigt.

Schreiben Sie eine main()-Funktion, welche die IntVector-Klasse analog zur main()- Funktion in Übung 2.2 benutzt.

Aufgabe 3.2

Windows verfügt über eine Funktion namens Beep(), welche einen Ton über den Lautspre- cher ausgibt. Die Funktion ist in der Header-Datei <windows.h> wie folgt deklariert:

bool Beep(unsigned int freq, unsigned int duration);

freq gibt die Tonhöhe an.

-

duration gibt die Dauer des Tons in ms an.

(a)

Schreiben Sie eine Klasse namens Beeper, welche wie folgt verwendet werden kann:

int main()

{

Beeper *beeper; beeper = new Beeper(500,1000); // Frequenz 500, Dauer 1000 ms

beeper->beep(); delete beeper;

// Tonausgabe

}

(b)

In gewissen Fällen erfordert es eine Anwendung, dass nur genau eine Instanz von einer Klasse erzeugt werden darf. Dieses kann auf folgende Weise erreicht werden:

- Der Konstruktor wird private deklariert.

- Eine als public definierte static-Elementfunktion create() prüft, ob bereits eine In- stanz existiert.

.

Falls nein, erzeugt create() eine neue Instanz und liefert einen Zeiger auf diese zu- rück.

.

Falls ja, liefert create() einen Zeiger auf die bereits existierende Instanz zurück.

(b1) Schreiben Sie die Klasse Beeper so um, dass nur eine Instanz erzeugt werden kann. (b2) Passen Sie die oben angegebene main()-Funktion in geeigneter Weise an.



Aufgabe 3.3

Die Klasse IntVector aus Übungsaufgabe 3.1 soll um einen sogenannten Iterator ergänzt werden. Hierbei handelt es sich um ein allgemeines Prinzip, welches einen sukzessiven Zu- griff auf alle Elemente eines Containers (im Beispiel ein IntVector-Objekt) ermöglicht.

Ein Iterator ist ein Objekt einer Klasse, welche über eine Elementfunktion next( ) verfügt. Diese liefert mit jedem Aufruf einen Zeiger auf ein Inhaltselement eines IntVector- Containers zurück. Beim ersten Aufruf ist dies ein Zeiger auf das 1. Listenelement, beim 2. Aufruf wird ein Zeiger auf das 2. Listenelement zurück geliefert, usw. Falls das Listenende erreicht ist, liefert next( ) einen NULL-Zeiger zurück. Der nachfolgende Programmausschnitt zeigt die Anwendung eines Iterators:

IntVector vector; int *el;

// vector fuellen

Iterator it(&vector);

while ((el = it.next()) != NULL) cout << *el << '\n';

Hinweise:

- Da das Iterator-Objekt Zugriff auf die privaten Daten des IntVector-Objekts benötigt, muss die Iterator-Klasse als friend der IntVector-Klasse definiert werden.

- Bei der Erzeugung eines Iterator-Objekts muss der Bezug zum jeweiligen IntVector- Objekt hergestellt werde. Hierzu muss das IntVector-Objekt als Zeiger oder Referenz an den Konstruktor der Iterator-Klasse übergeben werden.

Aufgabe 3.4 (Allgemeine Fragen)

(a)

Was bewirkt das Attribut const hinter der Deklaration einer Elementfunktion?

(b)

In einer Elementfunktion wird der Ausdruck this->x verwandt. Geben Sie eine alternative Schreibweise für den Ausdruck an.

(c)

Wann benötigt man eine Elementinitialisierungsliste?

(d)

Wie viele Konstruktoren kann eine Klasse haben?

(e)

Für einen Klassentyp Student existiert lediglich der folgende Konstruktor:

Student(long matrNr); Ist die Vereinbarung Student semester[30]; korrekt? Wenn nein, warum nicht?

(f)

Welche Aufgabe hat ein automatisch generierter Standardkonstruktor?

(g)

Warum kann man aus einer static-Elementfunktion nicht auf Instanzvariablen zugreifen?



4. Vererbung

Während das Klassenkonzept auch in prozeduralen Programmiersprachen enthalten ist, ist die Vererbung eine spezielle Eigenschaft der objektorientierten Programmierung.

Der Mechanismus der Vererbung erlaubt es, eine neue Klasse von einer (oder mehreren) bestehenden Klassen abzuleiten. Die abgeleitete Klasse erbt alle Eigenschaften und Fähig- keiten der bestehenden Klasse(n).

4.1 Abgeleitete Klassen

Die Klasse UndGatter aus Beispiel 3.1 soll so modifiziert werden, dass bei der Pro- grammausführung ein Log mit den ausgeführten Schaltvorgängen ausgegeben wird.

Bei herkömmlicher Programmierung würde man die Klasse UndGatter auf Quellpro- grammebene in geeigneter Weise modifizieren. Zur Realisierung der Protokollierungsfunkti- onalität sind folgende Erweiterungen erforderlich:

(1) Identifikation eines UndGatter-Objekts in Form eines druckbaren Namens (2) Erweiterung der Elementfunktionen UndGatter::Schalten() um die geforderte Logaus- gabe.

Deklaration einer abgeleiteten Klasse

Mit den Möglichkeiten der Vererbung kann die bestehende Klasse unverändert bleiben. Die geforderten Erweiterungen werden in einer abgeleiteten Klasse namens UndGatterMitLog realisiert. Hierbei wird auf die existierende Klasse UndGatter Bezug genommen.

Beispiel 4.1: ( Deklaration abgeleiteter Klassen)

enum Signal { L, H };

// Deklaration der Klasse UndGatter class UndGatter { Signal ein1; Signal ein2; Signal aus; public:

UndGatter(Signal s1, Signal s2); int setEingangsSignal(int nr, Signal sig); Signal getAusgangsSignal(void); void schalten(void);

};



Beispiel 4.1 (Forts.): ( Deklaration abgeleiteter Klassen)

// Deklaration der Klasse UndGatterMitLog

class UndGatterMitLog : public UndGatter {

char *printName;

public:

// druckbarer Name

UndGatterMitLog(Signal s1, Signal s2, char *name="noname"); void schalten(void); char* getPrintName();

};

Bemerkungen

- Die Deklaration einer abgeleiteten Klasse erfolgt unter Bezugnahme auf eine Basisklasse. - UndGatterMitLog enthält eine zusätzliche Instanzvariable printName, welche einen druckbaren Namen für das Gatter enthält, sowie eine zusätzliche Elementfunktion get- PrintName(), welche den Zugriff auf printName erlaubt. Weiterhin ist die Elementfunkti- on schalten() redefiniert.

Syntax: class class-name : { access-spec } opt class-name { member-liste

};

access-spec ::= { public | private } 1

- access-spec regelt für ein Objekt der abgeleiteten Klasse die Zugriffsmöglichkeiten zu den von der Basisklasse geerbten Elementen. Bei Angabe von public wird die Zugriffsspezifi- kation der Basisklasse übernommen; d.h private-Elemente bleiben private und public- Elemente bleiben public (näheres siehe 4.2).

Ein Objekt einer abgeleiteten Klasse verfügt über alle Instanzvariablen und Elementfunk- tionen der Basisklasse.

In der abgeleiteten Klasse können neue, zusätzliche Instanzvariablen und Elementfunktio- nen definiert werden. Darüber hinaus können geerbte Elementfunktionen redefiniert wer- den. Dabei verliert die geerbte Elementfunktion die Gültigkeit für Objekte der abgeleiteten Klasse.

Ableitungsbeziehungen drücken eine “ist ein”-Beziehung aus und werden wie folgt grafisch veranschaulicht:

UndGatter

und werden wie folgt grafisch veranschaulicht: UndG atter UndGatterMitLo g Abb. 4.1 : Ableitungsbeziehung 

UndGatterMitLog

Abb. 4.1: Ableitungsbeziehung



Wie Abb. 4.2 zeigt enthält das Speicherabbild eines Objekts einer abgeleiteten Klasse das Speicherabbild seiner Basisklasse; d.h. es enthält alle in der Basisklasse deklarierten In- stanzvariablen.

ein1 ein2 aus printName
ein1
ein2
aus
printName

Speicherbereich der Klasse UndGatter

Speicherbereich der Klasse UndGatterMitLog

Abb. 4.2: Speicherabbild der Klasse UndGatterMitLog

Elementfunktionen in abgeleiteten Klassen

Elementfunktionen einer abgeleiteten Klasse können auf public-Elemente der Basisklasse ohne Angabe des Objekts zugreifen; d.h. für Elemente der Basisklasse wird ebenfalls der Zeiger