Programmieren in C++

Werbung
ã Dr. Stefan Florczyk 2001
1
Elementare Bestandteile von C++ .............................................................................................................1
1.1
Deklarationen............................................................................................................. 1
1.2
Der Scope .................................................................................................................. 1
1.3
Lebensdauer............................................................................................................... 3
1.4
Identifizierer .............................................................................................................. 4
1.5
Typen ......................................................................................................................... 4
1.5.1
Elementare Typen.............................................................................................. 5
1.5.2
Implizite Typumwandlungen............................................................................. 5
1.5.3
Void ................................................................................................................... 5
1.6
Pointer........................................................................................................................ 6
1.7
Arrays ........................................................................................................................ 7
1.8
Strukturen .................................................................................................................. 8
1.9
Referenzen ............................................................................................................... 10
1.10 Konstanten ............................................................................................................... 11
1.11 Aufzählungen........................................................................................................... 12
1.12 Kommentare ............................................................................................................ 13
2
Operatoren ................................................................................................................................................13
2.1
2.2
2.3
2.4
2.5
3
Anweisungen..............................................................................................................................................21
3.1
3.2
3.3
3.4
4
Klammerung ............................................................................................................ 14
Inkrement und Dekrement-Operatoren.................................................................... 14
Bitweise logische Operatoren.................................................................................. 15
Typumwandlung...................................................................................................... 16
Der Heap (die Operatoren New und Delete) ........................................................... 17
Verzweigungen ........................................................................................................ 21
Der ?-Operator......................................................................................................... 22
Die switch-Anweisung ....................................................................................... 22
Goto ......................................................................................................................... 23
Funktionen und Dateien ...........................................................................................................................24
4.1
Das Verbinden von Dateien..................................................................................... 24
4.2
Bibliotheken............................................................................................................. 28
4.3
Funktionen ............................................................................................................... 30
4.3.1
Funktionen deklarieren .................................................................................... 30
4.3.2
Funktionen definieren...................................................................................... 30
4.3.3
Call by Reference and Call by Value .............................................................. 31
4.3.4
Überladen von Funktionen .............................................................................. 33
4.3.5
Default-Argumente.......................................................................................... 34
4.3.6
Zeiger auf Funktionen ..................................................................................... 34
4.4
Makros ..................................................................................................................... 35
5
Klassen .......................................................................................................................................................35
5.1
5.2
5.3
5.4
5.5
5.6
5.7
5.8
5.9
5.10
5.11
5.12
Eigenschaften und Methoden .................................................................................. 36
Der this-Zeiger .................................................................................................... 39
Get und Set Methoden ............................................................................................. 39
Konstante Klassenmethoden und konstante Objekte............................................... 41
Konstruktoren .......................................................................................................... 42
Destruktoren ............................................................................................................ 43
Kopier-Konstruktoren.............................................................................................. 49
Inline........................................................................................................................ 53
Friends ..................................................................................................................... 54
Qualifizierter Zugriff auf Klassen-Elemente........................................................... 58
Eingebettete Klassen................................................................................................ 58
Static-Elemente........................................................................................................ 62
I
5.13
6
6.1
6.2
6.3
6.4
6.5
7
Redefinition von Methoden der Basisklasse ........................................................... 68
Virtuelle Methoden.................................................................................................. 70
Abstrakte Klassen .................................................................................................... 75
Mehrfache Vererbung.............................................................................................. 77
Zugriffskontrolle...................................................................................................... 83
Operatoren überladen ..............................................................................................................................87
7.1
7.2
7.3
7.4
8
Pointer auf Klassen-Member ................................................................................... 62
Abgeleitete Klassen ...................................................................................................................................63
Möglichkeiten bei der Überladung von Operatoren ................................................ 89
Überladen des Zuweisungsoperators ....................................................................... 89
Überladen des Ausgabeoperators ostream ......................................................... 94
Überladung des Array-Operators............................................................................. 96
Templates...................................................................................................................................................96
II
1 Elementare Bestandteile von C++
1.1 Deklarationen
Um einen Identifizierer in einem C++-Programm verwenden zu dürfen, muß dieser deklariert werden. Die Deklaration informiert den Compiler darüber, daß der Identifizierer verwendet werden
darf. Sofern der Compiler für den Identifizierer Speicherplatz zur Verfügung stellt, liegt auch
gleichzeitig eine Definition vor. In den folgenden Beispielen werden einige elementare Datentypen
von C++ verwendet. Mit float Variablen werden ebenso wie mit double Variablen Gleitkommazahlen gespeichert, mit int Variablen Ganzzahlen und char Variablen eignen sich zur Aufnahme
von Zeichen. Mit 'structs', welche ebenfalls in dem Beispiel vorkommen, lassen sich verschieden
Datentypen zusammenfassen. Schließlich wird auch noch eine Funktion verwendet. Einer Funktion kann man ein oder mehrere Argumente übergeben. Nach Ausführung eines Funktionsrumpfes
liefert diese dann ein Ergebnis oder die leere Wertemenge void an die aufrufende Umgebung
zurück:
char ch;
int i = 10;
struct rechteck {float laenge, breite};
extern float flaeche(rechteck);
Die Identifizierer ch, i und rechteck sind auch gleichzeitig definiert worden, so daß ein angemessener Speicherbereich vorgesehen ist.
Der mit dem Schlüsselwort extern deklarierte Funktionsrumpf flaeche ist hier nicht definiert worden, die Definition erfolgt an anderer Stelle. Die mehrfache Definition von Namen ist in einem
C++-Programm nicht zugelassen. Hingegen dürfen Namen durchaus mehrfach deklariert werden,
sofern die Typen identisch sind wie das folgende Beispiel veranschaulicht:
double d;
// dies ist eine unzulässige doppelte Definition
double d;
extern int i;
// dies ist eine zulässige mehrfache Deklaration, beide
// Deklarationen haben den gleichen Typ int
extern int i;
1.2 Der Scope
Durch die Deklaration eines Namens in einem 'Scope' wird sichergestellt, daß der Name nur innerhalb eines Bereiches verwendet werden darf.
Sofern ein Name innerhalb einer Funktion deklariert wurde, gilt der 'Scope' vom Zeitpunkt der Deklaration und reicht bis zum Ende des Blockes, in dem die Deklaration durchgeführt wurde. In C++
beginnt ein Block mit einer öffnenden geschweiften Klammer und endet mit einer schließenden
geschweiften Klammer. Namen die außerhalb einer Funktion oder Klasse definiert wurden, werden als global bezeichnet und gelten von der Deklaration an bis zum Ende der Datei, in welcher
die Deklaration erfolgte.
Sollte ein Name innerhalb eines Blocks deklariert worden sein, welcher von einem weiteren Block
umschlossen wird, in dem eine Deklaration mit dem gleichen Namen durchgeführt wird, dann wird
der Name des umschließenden Blocks im inneren Block verdeckt.
In dem folgenden Beispiel wird ein sogenannter 'Standard Header' eingebunden. Dies ist hier iostream.h. iostream.h deklariert Objekte, welche zum Schreiben und Lesen auf die Standardeingabe und Ausgabe verwendet werden. Mit cout werden Ausgaben auf die Standardausgabe (meistens der Bildschirm) durchgeführt und mit cin Eingaben auf die Standardeingabe (meistens die
Tastatur). In C++ steht ein Präprozessor zur Verfügung. Präprozessorbefehle beginnen mit einer
1
einer Raute (#). 'Standard Headers' werden mit spitzen Klammern eingebunden. 'Standard Headers' sind genau genommen kein Bestandteil von C++, sollen aber vom Hersteller des C++ Compilers zur Verfügung gestellt werden. Informationen zu den 'Standard Headers' können daher aus
der Dokumentation und den Kommentaren in den 'Headers Files' entnommen werden. Unter Unix
sind die C und C++ 'Headers Files' in der Regel im Verzeichnis /usr/include zu finden. Wobei
für die C++ 'Headers Files' noch ein weiteres Unterverzeichnis wie beispielsweise /g++ oder /CC
angelegt sein sollte.
Im Beispiel werden Zeiger auf char Variablen verwendet (char*). Diese Zeiger verweisen auf eine
Speicheradresse. Die allgemeine Form lautet ('type*'). Wobei 'type' für einen beliebigen Datentyp
steht. Die Speicheradresse kann dann eine Variable von 'type' aufnehmen. Zeichenketten werden
in C++ z.B. mit char str[] = "Zeichenfolge" deklariert. Es handelt sich hierbei um einen
Array, welcher mehrere Elemente des gleichen Typs aufnehmen kann. Der Array hat hier die Länge 13, da Zeichenfolge zwölf Zeichen beinhaltet und an jede Zeichenkette noch das Terminalzeichen \0 angehängt wird, um das Ende anzuzeigen. Sofern in einem C++ Programm lediglich
der Feldname ohne Indexoperator ([]) vorkommt, wird eine Standardkonversion in einen Zeiger
durchgeführt, der auf die Speicheradresse des ersten Elements des Array verweist. Dieser Umstand macht es in C++ Programmen möglich, daß man für die Anweisung char str[] = "Zeichenfolge" auch die Anweisung char* str = "Zeichenfolge" verwenden kann. Mit der
Escapesequenz \n wird der Cursor auf das Zeilenende gesetzt. Mit Escapesequenzen kann man
Zeichen verarbeiten, welche nicht direkt über die Tastatur eingegeben werden können. Escapesequenzen beginnen immer mit einem Backslash (\).
Schließlich sei noch zum Verständnis des Beispiels gesagt, daß jedes C++ Programm genau eine
Funktion mit dem Namen main enthalten muß. In der im Beispiel verwendeten Alternative bekommt main zwei Argumente übergeben. Das erste Argument argc enthält die Anzahl der Argumente, die beim Aufruf des Programms von der Betriebssystemumgebung übergeben werden.
Diese Argumente werden in der Variablen argv abgelegt und können über einen Index mit dem
Wertebereich [0; argc] abgerufen werden. argv[0] enthält dabei den Namen des Programms. Bei
argv handelt es sich also um einen Zeiger, der auf ein zweidimensionales Array verweist. Man
kann ebenso die Schreibweise **argv anstelle von *argv[] verwenden. Alternativ hat man die
Möglichkeit, die argumentlose Form von main zu verwenden. Man kann also beispielsweise auch
void main() schreiben:
#include <iostream.h>
char* str = "Globale Variable\n";
// str ist global
void f()
{
char* str ="Lokale Variable im 1. Block\n";
// hier wird str überdeckt
cout <<str;
{
char * str = "Lokale Variable im 2. Block\n"; // hier wird die
// lokale Variable des 1. Blocks überdeckt
cout <<str;
}
}
void main(int argc, char * argv[])
{
cout <<"Ausgabe der globalen Variablen:\t "<<str;
cout <<"Aufruf von f:\n";
f();
2
}
Um dennoch auf verdeckte Variablen zugreifen, muß man den 'Scope-Resolution-Operator' ::
einsetzen. Dies ist allerdings nur für verdeckte globale Variablen möglich. Auf verdeckte lokale
Variablen kann so nicht zugegriffen werden:
#include <iostream.h>
char* str = "Globale Variable\n";
//
str ist global
void f()
{
char* str ="Lokale Variable\n";
// hier wird das globale str überdeckt
cout <<str;
::str = "Zugriff auf globale Variable erfolgt\n"; // hier wird auf die
// verdeckte globale Variable zugegriffen
}
void main(int argc, char * argv[])
{
cout <<"Ausgabe der globalen Variablen vor dem Aufruf von f:\n "<<str;
cout <<"Aufruf von f:\n";
f();
cout <<"Ausgabe der globalen Variablen nach dem Aufruf von f:\n "<<str;
}
Es ist nicht zulässig, ein Funktions-Argument in der entsprechenden Funktion zu verdecken.
void f(char * ch)
{
char ch;
}
1.3 Lebensdauer
Die Erzeugung eines Objekts erfolgt im Programmlauf, wenn auf seine Definition getroffen wird.
Sobald der 'Scope' des Objektes verlassen wird, wird auch das Objekt gelöscht und somit belegter
Speicher wieder freigegeben. Für globale Objekte folgt daraus, daß diese bis zum Ende des Programms bestehen bleiben. Diesen Effekt kann man auch für lokale Variablen erreichen, indem
man das Schlüsselwort static verwendet. In dem nun folgenden Beispielprogramm wird die Escapesequenz \t verwendet, welche einen horizontalen Tabulator repräsentiert:
#include <iostream.h>
void f()
{
int j = 10;
// diese Initialisierung wird bei jedem Aufruf von f
// durchgeführt;
static int i = j;
// diese Initialisierung wird nur einmal
// durchgeführt;
3
j++;
i++;
cout << "Ausgabe von j:\t"<< j << "\t" << "Ausgabe von i\t" <<i<< "\n";
}
void main(int argc, char * argv[])
{
for ( int i=0; i < 5; i++)
{
f();
}
}
In diesem Beispiel wird bei jedem erneuten Aufruf von f die Variable j mit 10 initialisiert. Die static deklarierte Variable i behält jedoch ihren Wert auch noch nach dem Verlassen der Funktion.
1.4 Identifizierer
Identifizierer müssen mit einem Buchstaben oder Unterstrich _ beginnen. Danach können weitere
Buchstaben oder Ziffern folgen. Grundsätzlich ist in C++ die Länge eines Namens nicht beschränkt. Die Verwendung von C++ Schlüsselworten als Name ist nicht zulässig. In C++ wird allerdings zwischen der Groß- und Kleinschreibung unterschieden. Beispielsweise ist struct in C++
ein Schlüsselwort und darf daher als Identifizierer nicht verwendet werden. STRUCT dagegen ist
aber als Identifizierer erlaubt.
1.5 Typen
Zu jedem Identifizierer muß ein Typ angegeben werden. Hiermit wird spezifiziert, welche zulässigen Werte der Identifizierer annehmen darf. Mit sizeof kann man den Speicherbedarf eines Objekts abfragen. Für den Speicherbedarf der Fließkommatypen gilt sizeof(float) ≤ sizeof(double) ≤ sizeof (long double). Das folgende Beispiel demonstriert die Verwendung von
sizeof:
#include <iostream.h>
void main(int argc, char * argv[])
{
long double d;
cout << "Speicherbedarf von d:"<< sizeof(d);
}
In C++ ist auch möglich explizit Typkonversionen vorzunehmen:
#include <iostream.h>
void main(int argc, char * argv[])
{
float f = 10.567;
int i = int (f);
cout <<i;
}
4
Bei solchen Typkonversionen muß man aber beachten, daß es zu Informationsverlust kommen
kann. Das obige Beispiel wird beispielsweise vom Gnu-Compiler unter Linux ohne Fehlermeldung
übersetzt. Zwangsläufig muß aber bei der Typkonversion der Nachkommawert abgeschnitten
werden.
Resümee: Typkonversionen sind mit Vorsicht zu genießen.
1.5.1 Elementare Typen
Bei den folgenden elementaren Typen handelt es sich um Integer-Typen:
char; short int; int; long int.
Für Fließkommazahlen verwendet man:
float; double; long double.
Weiter gibt es Typen für vorzeichenlose Werte:
unsigned char; unsigned short int; unsigned int; unsigned long int.
Für vorzeichenbehaftete Felder existieren ebenfalls entsprechende Typen:
signed char; signed short int; signed int; signed long int.
Wenn im Programm für einen Identifizierer kein Typ angegeben wird, dann erfolgt automatisch
eine Assoziation mit int, sofern weitere Schlüsselwörter wie signed, long, const usw. verwendet
werden. Um die Lesbarkeit des Programms zu erhöhen, sollte aber auch in diesen Fällen ein Typ
angegeben werden.
Der Typ bool umfaßt lediglich die beiden Werte true und false.
1.5.2 Implizite Typumwandlungen
Neben der beschriebenen expliziten Konvertierung, kann auch eine automatische implizite Typkonversion durchgeführt werden. Die Gefahr des Informationsverlustes ist in solchen Fällen u.U.
noch größer, da sich der Programmierer eventuell der vorgenommen Typumwandlung nicht bewußt ist:
#include <iostream.h>
void main(int argc, char * argv[])
{
float f = 20.567;
int i = f;
cout <<i;
}
Wenn man das angeführte Programm übersetzt, dann erhält man vom Compiler eine Warnmeldung, welche auf die Typkonversion aufmerksam macht. Das Programm ist aber lauffähig und
liefert als Ergebnis 20.
1.5.3 Void
Der Typ void wird dazu verwendet, um auf den Umstand hinzuweisen, daß eine Funktion keinen
Rückgabewert returniert:
void f()
{
a = a+1; // a sei eine globale Variable
}
5
In diesem Beispiel besteht die Wirkung der Funktion darin, daß Sie den Wert einer globalen Variablen verändert.
1.6 Pointer
Pointer verweisen auf eine Speicheradresse. Hierfür verwendet man den *-Operator, um einen
Pointer zu deklarieren. Will man im Programm auf den Wert zugreifen, welcher in der Speicheradresse abgelegt ist, auf welcher der Pointer verweist, so verwendet man den *-Operator als Dereferenzierungsoperator. Mit dem Schlüsselwort new wird für den Datentyp auf den der Pointer
verweisen soll, Speicherplatz im 'Heap' geschaffen und mit delete wieder freigegeben. Damit
besteht die Möglichkeit, die Speicherverwaltung für die Variable im 'Heap' losgelöst vom
Programmablauf, bei welchem die Variablen im 'Stack' verwaltet werden, zu übernehmen. Ein
kleines Beispielprogramm soll den Umgang mit Pointern verdeutlichen:
#include <iostream.h>
void main(int argc, char * argv[])
{
int * pi = new int;
// hier wird ein Pointer auf int definiert
// und Speicherbereich im Heap zur Verfügung
// gestellt
int i = 10;
*pi = i;
// Der Speicher auf den pi verweist, erhält den
//Wert von i zugewiesen
cout<< "Wert von pi:\t" <<pi<<
"\n Wert von *pi:\t"<<*pi<< "\n Wert von i:\t"<<i;
delete pi;
// Reinigen des Heaps bevor neue Zuweisung erfolgt
pi = &i;
// pi erhält die Speicheradresse von i im Stack zugewiesen
*pi = 20;
// Der Speicher auf den pi und i verweisen, erhält die 20
//zugewiesen
cout<< "\n Wert von pi:\t" <<
pi<< "\n Wert von *pi:\t"<<*pi<< "\n Wert von i:\t"<<i<<"\n";
}
Man erhält als Ausgabe:
Wert von pi:
0xbffff748
Wert von *pi:
10
Wert von i: 10
Wert von pi:
0xbffff724
Wert von *pi:
20
Wert von i: 20
Zunächst wir im Programm der Pointer pi mit dem *-Operator definiert. Weiter wird eine int Variable mit 10 initialisiert. Mit dem *-Dereferenzierungsoperator kann der Speicherwert, auf den pi
verweist, verändert werden. Hier wird der Wert von i zugewiesen. Nun wird zunächst die Variable
im 'Heap' gelöscht und anschließend wird mit dem Adreß-Operator & auf die Speicheradresse von
i im 'Stack' zugegriffen und pi zugewiesen. Dies hat zur Folge, daß durch Änderung des Wertes
von *pi sich gleichzeitig auch der Wert von i verändert.
6
Eine weitere Eigenschaft von Pointern ermöglicht es, daß auf ihnen arithmetische Berechnungen
durchgeführt werden können:
#include <iostream.h>
void main(int argc, char * argv[])
{
int * pi = (int*) 0x60;
cout <<"Speicherbedarf von pi: \t"<<sizeof(pi)<<"\n Speicheradresse von
pi:\t" <<pi;
pi++;
cout <<"\n Speicheradresse von pi nach der arithmetischen Operation:\t" <<
pi<<"\n";
}
Das Programm liefert folgende Ausgabe:
Speicherbedarf von pi:
4
Speicheradresse von pi:
0x00000060
Speicheradresse von pi nach der arithmetischen Operation:
0x00000064
Hier wird ein Zeiger auf int definiert, welcher eine Adresse im hexadezimalen Format zugewiesen
bekommt. Hierzu wird die Zeichenfolge 0x verwendet. Dann wird mit sizeof der belegte Speicherplatz von pi angegeben. Dieser beträgt 4 Byte. Der Speicherplatzbedarf hängt vom zugrundeliegenden Typ ab und kann auf verschiedenen Maschinen unterschiedlich sein. Durch Ausgabe
der Speicheradressen vor und nach der arithmetischen Operation erkennt man, daß die Speicheradresse von pi um 4 Byte erhöht wurde.
1.7 Arrays
Bei Arrays handelt es sich um Konstrukte, in welchen gleichartige Typen über einen Index angesprochen werden können:
#include <iostream.h>
void main(int argc, char * argv[])
{
char zweidim [8][8];
char z = 65;
for (int i = 0; i < 8; i++)
{
for (int j = 0; j < 8; j++)
{
zweidim [i][j] =z++;
cout << zweidim [i][j];
}
cout <<"\n";
}
}
7
Wie an dem Programm ersichtlich ist, beginnt der Index bei Arrays mit 0. Wird also ein eindimensionaler Array der Größe 8 definiert, so beginnt der Array mit 0 und endet mit 7.
Im Beispielprogramm wurde ein zweidimensionales Array mit 64 Charakter-Feldern erzeugt. Arrays lassen sich sehr gut mit for-Schleifen bearbeiten. Im Falle eines Zweidimensionalen Arrays
bieten sich zwei verschachtelte for-Schleifen an. Hier wird eine Hilfsvariable verwendet, mit welcher die 64 Felder mit den Buchstaben des Alphabets und einiger Sonderzeichen initialisiert und
ausgegeben werden. Nachdem eine komplette Zeile ausgegeben wurde, erfolgt ein Zeilenvorschub. Solche zweidimensionalen Arrays eignen sich beispielsweise zur Repräsentation von Matrizen. Für die Darstellung von Vektoren verwendet man hingegen eindimensionale Arrays. Das
mit dem Integer-Wert korrespondierende Zeichen kann aus einer ASCII-Tabelle entnommen werden. Die Anfangsinitialisierung von z mit 65 repräsentiert beispielsweise den Großbuchstaben A.
Eine weitere Möglichkeit zur Initialisierung von Arrays wird nun demonstriert:
char eindim[] = {'1', '2', '3', '4', '5'};
An diesem Beispiel erkennt man, daß die Größe vom Programmierer nicht festgelegt werden
braucht, sondern vom Compiler automatisch ermittelt werden kann.
Arrays vom Typ char bieten weiter die Möglichkeit, diese direkt mit einer Zeichenkette zu initialisieren, so daß folgendes auch korrekt wäre:
char eindim[] = "12345";
Arrays bieten die Besonderheit, daß ihr Name als Pointer auf das erste Array-Element fungiert,
wie dies nun verdeutlicht wird:
#include <iostream.h>
void main(int argc, char * argv[])
{
int ia[10] = {1,2,3,4,5,6,7,8,9,0};
int *ip = ia;
// int *ip = &ia[0]; diese Anweisung wäre gleichwertig
while (*ip)
{
cout<<*ip;
ip++;
}
}
Hier wird ein Pointer auf Integer mit der Adresse des ersten Array-Elements initialisiert. Das letzte
Array-Element nimmt den Wert 0 an und repräsentiert in C++ den Wert false, so daß dieses Element als Abbruchkriterium für die while-Schleife herangezogen werden kann.
Innerhalb der while-Schleife erfolgt die Ausgabe des Werts, auf welchen der Pointer augenblicklich verweist. Anschließend wird der Pointer mit Hilfe der Zeigerarithmetik weitergesetzt.
1.8 Strukturen
Strukturen bieten im Unterschied zu Arrays die Möglichkeit unterschiedliche Datentypen zusammenzufassen:
struct buch
{
char* buchtitel;
int
seitenzahl;
int kapitelzahl;
8
char* bestellnummer;
};
Mit dieser Struktur werden die zu erfassenden Eigenschaften eines Buches festgelegt. Innerhalb
des Programms können dann Variablen der Struktur definiert werden. Der Zugriff auf die Elemente
der Struktur erfolgt über einen Punkt-Operator:
#include <iostream.h>
void main(int argc, char * argv[])
{
struct buch
{
char* buchtitel;
int
seitenzahl;
int kapitelzahl;
char* bestellnummer;
};
buch b1;
b1.buchtitel = "Rotkäppchen";
b1.seitenzahl = 80;
b1.kapitelzahl = 6;
b1.bestellnummer = "1313";
}
Wenn ein Zeiger auf solch eine Struktur definiert wird, wird anstelle des Punkt-Operators der -> Operator verwendet:
#include <iostream.h>
void main(int argc, char * argv[])
{
struct buch
{
char* buchtitel;
int
seitenzahl;
int kapitelzahl;
char* bestellnummer;
};
buch *b1 = new buch;
b1->buchtitel = "Rotkäppchen";
b1->seitenzahl = 80;
b1->kapitelzahl = 6;
b1->bestellnummer = "1313";
delete b1;
}
9
1.9 Referenzen
Mit Referenzen sind Alias-Namen für Identifizierer gemeint:
#include <iostream.h>
void main(int argc, char * argv[])
{
double d = 1.0;
double& f = d;
f = 7.0;
cout<<"Wert von d:\t" <<d;
}
In diesem Beispiel ist f lediglich ein Alias-Name für d. Der Wert von d beträgt ebenfalls 7, nachdem f auf 7 geändert wurde.
Referenzen können als Funktionsargumente verwendet werden. Es ergibt sich dadurch das 'Callby-Reference' Konstrukt, welches es ermöglicht, den per 'Reference' übergebenen Funktionswert
in der Funktion zu verändern. Außerdem kann durch diese Vorgehensweise Speicherplatz eingespart werden, da bei der Übergabe als Referenz keine Kopie von der Variable angelegt werden
muß:
#include <iostream.h>
void f(int& i)
{
i++;
}
void main(int argc, char * argv[])
{
int a = 10;
f(a);
cout<<"Wert von a: \t"<<a;
}
Die in main definierte Variable a wird per Referenz an f übergeben, so daß das Funktionsargument i lediglich ein Alias für a ist. Die Inkrementierung von i in f erhöht also auch a um eins, so
daß das Resultat in main nach Aufruf von f für a 11 beträgt.
Diesen Effekt kann man auch erreichen, indem man als Funktionsargument einen Pointer spezifiziert, an welchem man die Adresse eines Identifizierers übergibt:
#include <iostream.h>
void
f(int* i)
{
(*i)++;
}
10
void main(int argc, char * argv[])
{
int a = 10;
f(&a);
cout<<"Wert von a: \t"<<a;
}
Auch hier erhält man für a wiederum 11 ausgegeben, so daß man durch Verwendung von Pointern ebenfalls einen 'Call-by-Reference' Effekt erzielen kann.
1.10 Konstanten
In den bisherigen Beispielprogrammen wurde mit Variablen gearbeitet, deren Werte sich im Laufe
eines Programmablaufs ändern können. Verwendet man in C++ das Schlüsselwort const ist eine
Wertänderung nicht möglich. Der gesetzte Wert, was im Falle eines Pointers auch eine Speicheradresse sein kann, wird während des gesamten Programmlaufs beibehalten. Sofern ein Pointer
auf eine Konstante definiert wird, ist es allerdings möglich, den Speicherwert des Pointers zu verändern. Ist dagegen der Pointer konstant, so darf man seine Speicheradresse nicht verändern,
gleichwohl darf man den Wert, auf den der Pointer verweist, verändern, wenn dieser nicht auch
eine Konstante ist. Es folgen einige Beispiele, mit denen Konstanten deklariert werden:
const int i = 10;
const char* ch1 = "1234"; // Pointer auf eine Konstante
char* const ch2 = "1234"; // Konstanter Pointer
Im ersten Beispiel wird i als konstanter Integerwert definiert, so daß der Wert 10 während des
gesamten Programmlaufs beibehalten werden muß. Der Versuch den Wert 10 verändern zu wollen, würde dazu führen, daß der Compiler eine Fehlermeldung ausgibt.
Die beiden folgenden Beispiele müssen differenziert betrachtet werden.
Im zweiten Beispiel wird ein Pointer auf eine Konstante gesetzt. Die Konstante selbst darf daher
nicht verändert werden, es ist aber möglich dem Pointer eine andere Speicheradresse zuzuweisen. Im letzten Beispiel wird ein konstanter Pointer definiert. Dies bedeutet, daß es möglich ist,
den Wert, auf den der Pointer verweist, zu modifizieren, sofern es sich bei dem zugewiesenen
Wert nicht ebenfalls um eine Konstante handelt, wie dies hier der Fall ist. Man darf allerdings auf
keinen Fall den Pointer auf eine andere Speicheradresse setzen. Diese Ausführungen werden nun
im folgenden Programm demonstriert:
#include <iostream.h>
void main(int argc, char * argv[])
{
const int
i = 10;
char*
tmp = "test";
const char*
ch1 = "1,2,3";
char*
ch2 = "4,5,6";
char
char*
const
ch3[] = {'t','e','s','t','\0'};
const
ch4 = ch3;
// flache Kopie
// ch4 und ch3 verweisen nun auf dieselbe
// Speicheradresse
11
cout <<"Wert von ch1:\t"<<ch1<<"\t"<<"\n";
cout <<"Wert von ch2:\t"<<ch2<<"\t"<<"\n";
cout <<"Wert von ch3 \t"<<ch3<<"\t"<<"\n";
cout <<"Wert von ch4 \t"<<ch4<<"\t"<<"\n";
ch1 = "9,10,11";
ch2 = "7,8,9";
i = 20;
// unzulässig
// unzulässig
ch1[0] = '4';
// unzulässig
ch2[0] = '7';
// bewirkt einen Laufzeitfehler da
// konstanter Pointer auf eine Konstante verweist
cout <<"Wert von ch1:\t"<<ch1<<"\t"<<"\n";
// nun erfolgt die Modifikation des Variablen Wertes auf den der
// konstante Pointer ch4 verweist
ch4[0] = 'T';
cout <<"Wert von ch3 \t"<<ch3<<"\t"<<"\n";
cout <<"Wert von ch4 \t"<<ch4<<"\t"<<"\n";
}
Als Ausgabe erhält man:
Wert von ch1:
1,2,3
Wert von ch2:
4,5,6
Wert von ch3
test
Wert von ch4
test
Wert von ch1:
9,10,11
Wert von ch3
Test
Wert von ch4
Test
1.11 Aufzählungen
Aufzählungen bieten sich an, wenn gleichartige Werte zusammengefaßt werden sollen. Die Elemente, welche in die Aufzählung aufgenommen werden sollen, beginnen per Default mit der Null
und werden aufsteigend mit Integer-Werten durchnumeriert. Es handelt sich bei den Elementen
also um Integer-Konstanten. Eingeleitet wird eine Aufzählung mit dem Schlüsselwort enum.
enum farben {rot, gruen, blau};
Bei diesem Beispiel ist rot eine Konstante für den Integer-Wert 0, gruen eine Konstante für den
Integer-Wert 1 und blau eine Konstante für den Integer-Wert 2.
Ein kleines Beispiel soll den Umgang mit Aufzählungen verdeutlichen:
#include <iostream.h>
void main(int argc, char * argv[])
{
enum farben {rot, gruen, blau};
12
int i;
for (int j = 0; j < 3; j++)
{
cout<<farben(j)<<"\n";
}
i = blau;
cout <<"Blau="<<i<<"\n";
farben gelb =farben(i);
// Achtung hier ist ein expliziter
// Cast notwendig
cout <<"Gelb="<<gelb<<"\n";
}
In dem Programm wird eine Schleife durchlaufen, in welcher die Integer-Werte der Aufzählungskonstanten ausgegeben werden. Weiter ist ersichtlich, daß Aufzählungskonstanten einem IntegerWert zugewiesen werden dürfen. Im umgekehrten Fall ist aber ein expliziter 'Cast' erforderlich.
1.12 Kommentare
Um die Wartung von Programmen zu erleichtern, kann der Programmcode um Kommentare ergänzt werden. Kommentare sollen nicht den Ablauf eines Algorithmus erläutern. Kommentare sollen eingesetzt werden, um zu erläutern, was mit einem Algorithmus oder einer Funktion beabsichtigt wird. Einzeiligen Kommentaren werden zwei Schrägstriche // vorangestellt. Kommentare,
welche sich über mehrere Zeilen erstrecken sollen, beginnen mit einem Schrägstrich gefolgt von
einem Stern /* und enden mit einem Stern gefolgt von einem Schrägstrich */:
person* init (char* name, char* vorname,int id)
/*
Die Funktion init(char*, char*, int)
erzeugt ein neues Objekt vom Typ Person und
initialisiert deren Eigenschaften
mit den Funktionsargumenten für den
Namen, Vornamen und die ID
*/
{
person* p = new person; // hier wird ein neues Objekt erzeugt
p->name = name;
p->vorname = vorname;
p->id = id;
return p;
}
Übung:
1. Programmieren Sie das bekannte 'Hello-World-Programm' in C++.
2 Operatoren
An dieser Stelle wird kein vollständiger Überblick über die in C++ zur Verfügung stehenden Operatoren gegeben. Hier sei auf die zahlreich vorhanden Handbücher verwiesen. Es werden hier einige
Beispiele demonstriert, um den generellen Umgang von Operatoren in C++ zu üben.
Zunächst sei angemerkt, daß es in C++ rechts-assoziative und links-assoziative Operatoren gibt.
Mit der Assoziativität wird festgelegt, in welcher Reihenfolge Ausdrücke ausgewertet werden. Will
13
man die dadurch festgelegte Auswertungsreihenfolge ändern, kann man dies durch Klammerung
erreichen. Generell gilt, daß 'unäre' und Zuweisungsoperatoren rechts-assoziativ und alle anderen
link-assoziativ sind. Die Reihenfolge, in welche Ausdrücke ausgewertet werden, richtet sich weiter
nach der Priorität, welche ebenfalls aus Tabellen von Handbüchern entnommen werden kann.
Der Ausdruck -i++ wird als -(i++) ausgewertet, da es sich hier um 'unäre' Operatoren handelt,
welche rechts-assoziativ sind und beide Operatoren dieselbe Priorität besitzen. Der Multiplikationsoperator ist dagegen beispielsweise links-assoziativ. Seine Auswertung erfolgt also in der Reihenfolge von links nach rechts:
i*j*k wird also ausgewertet als (i*j)*k.
Der Ausdruck i-j/k wird ausgewertet als i-(j/k). Alle beteiligten Operatoren sind linksassoziativ, aber der Operator / hat eine höhere Priorität als der - Operator, woraus sich die entsprechende Klammerung ergibt.
2.1 Klammerung
Die Auswertungsreihenfolge, welche sich aufgrund der Assoziativität und der Prioritäten ergibt,
kann durch das Setzen von Klammern beeinflußt werden.
Durch das Setzen von Klammern kann beispielsweise eine andere Auswertung des bereits vorgestellten Ausdrucks i-j/k erfolgen:
(i-j)/k.
Hier wird mit der Klammerung erreicht, daß der Vorrang des / Operators aufgehoben wird.
2.2 Inkrement und Dekrement-Operatoren
Mit dem Inkrement-Operator ++ und dem Dekrement-Operator -- werden Werte rauf- bzw. runtergezählt. Weiter entscheidet die Position des Inkrement- bzw. Dekrement-Operators im Ausdruck
zu welchem Zeitpunkt das herauf- bzw. herunterzählen durchgeführt wird. Der Einsatz dieser beiden Operatoren wird an einem kleinen Beispielprogramm demonstriert:
#include <iostream.h>
void main(int argc, char * argv[])
{
int i,j,k, zaehler;
i = 10;
zaehler = 0;
while (i)
{
j = i--;
// i-- entspricht i = i-1, wobei diese Zuweisung
//erst nach Abarbeitung der Anweisung (ebenfalls eine
//Zuweisung)durchgeführt wird
k = ++zaehler;
// ++zaehler entspricht zaehler = zaehler+1
cout<<"Wert von j:\t"<<"beim\t"<<zaehler<<". Durchlauf der Schleife:\t"<<j<<"\n";
cout<<"Wert von k:\t"<<"beim\t"<<zaehler<<". Durchlauf der Schleife:\t"<<k<<"\n";
}
}
Es ergibt sich folgende Ausgabe:
14
Wert von j: beim
1. Durchlauf der Schleife:
10
Wert von k: beim
1. Durchlauf der Schleife:
1
Wert von j: beim
2. Durchlauf der Schleife:
9
Wert von k: beim
2. Durchlauf der Schleife:
2
Wert von j: beim
3. Durchlauf der Schleife:
8
Wert von k: beim
3. Durchlauf der Schleife:
3
Wert von j: beim
4. Durchlauf der Schleife:
7
Wert von k: beim
4. Durchlauf der Schleife:
4
Wert von j: beim
5. Durchlauf der Schleife:
6
Wert von k: beim
5. Durchlauf der Schleife:
5
Wert von j: beim
6. Durchlauf der Schleife:
5
Wert von k: beim
6. Durchlauf der Schleife:
6
Wert von j: beim
7. Durchlauf der Schleife:
4
Wert von k: beim
7. Durchlauf der Schleife:
7
Wert von j: beim
8. Durchlauf der Schleife:
3
Wert von k: beim
8. Durchlauf der Schleife:
8
Wert von j: beim
9. Durchlauf der Schleife:
2
Wert von k: beim
9. Durchlauf der Schleife:
9
Wert von j: beim
10. Durchlauf der Schleife:
1
Wert von k: beim
10. Durchlauf der Schleife:
10
Die Steuerung des Abbruchkriteriums der Schleife erfolgt über eine Integer-Variable, welche bei
jedem Schleifendurchlauf um einen Wert heruntergezählt wird. Sobald i den Wert 0 einnimmt,
wird die Schleife beendet, da 0 als false interpretiert wird.
An dem Programm wird auch deutlich, daß die Position des Inkrement- bzw. Dekrement-Operators
unterschiedliche Auswirkungen hat. Ein vorangestellter Operator bewirkt, daß das Inkrement bzw.
Dekrement noch vor dem Ausführen der Anweisung (hier die Zuweisung) durchgeführt wird, ein
nachgestellter Operator bewirkt, daß die Anweisung erst durchgeführt wird, bevor die Inkrementierung bzw. Dekrementierung erfolgt.
2.3 Bitweise logische Operatoren
Mit bitweisen logischen Operatoren wie &, |, ^, << und >> werden Operationen auf der dualen Repräsentation von Integer-Werten durchgeführt. Mit dem &-Operator (logisches und) werden im Resultat diejenigen Bits gesetzt, welche bei den beiden, an der Operation beteiligten Argumente,
ebenfalls gesetzt waren. Beim |-Operator (logisches oder) werden im Resultat der Operation diejenigen Bits gesetzt, welche bei einem oder beiden der beteiligten Argumente gesetzt sind. Der ^
-Operator repräsentiert das logische oder. Mit diesem Operator werden im Resultat diejenigen Bits
gesetzt, welche bei einem der beiden beteiligten Argumente gesetzt sind, nicht aber bei beiden
gleichzeitig. Der Verschiebeoperator << führt eine Verschiebung der Bits des beteiligten linken
Arguments um den Wert des rechten Arguments nach links durch. Dabei erfolgt rechts eine Auffüllung mit 0-Bits entsprechend dem Wert des beteiligten rechten Arguments. Der Verschiebeoperator >> führt eine Verschiebung der Bits des beteiligten linken Arguments um den Wert des rechten
Arguments nach rechts durch. Dabei erfolgt links eine Auffüllung mit 0 Bits entsprechend dem
Wert des beteiligten rechten Arguments.
Diese Ausführungen können nun an einem kleinen Programm nachvollzogen werden. Zur
Verdeutlichung ist in den Programmkommentaren jeweils die duale Repräsentation der beteiligten
Integer-Werte und des Resultats aufgeführt.
15
#include <iostream.h>
void main(int argc, char * argv[])
{
int i,j,k;
i = 6;
j = 5;
// entspricht 00000110
// entspricht
k = i&j; // entspricht
00000101
00000100
cout<<"Wert von k:\t"<<k<<"\n";
k = i|j; // entspricht
00000111
cout<<"Wert von k:\t"<<k<<"\n";
k = i^j; // entspricht
00000011
cout<<"Wert von k:\t"<<k<<"\n";
k = i<<2; // entspricht
00011000
cout<<"Wert von k:\t"<<k<<"\n";
k = i>>2; // entspricht
00000001
cout<<"Wert von k:\t"<<k<<"\n";
}
Das Programm liefert die Ausgabe:
Wert von k: 4
Wert von k: 7
Wert von k: 3
Wert von k: 24
Wert von k: 1
2.4 Typumwandlung
In C++ kann die Typumwandlung auf zwei Arten durchgeführt werden. Da C++ auf C aufsetzt, ist
es möglich eine Typumwandlung mit der C-Notation durchzuführen:
int i = 10;
float f;
f = (float)i;
In typischer C++ Notation würde diese Typumwandlung so geschrieben werden:
int i = 10;
float f;
f = float(i);
Diese Notation erinnert mehr an einen Funktionsaufruf und wurde schon bei der Erläuterung des
Enumeration-Konstrukts kennengelernt.
Wenn die C++ Notation verwendet werden soll, um eine Typumwandlung, an der ein zusammengesetzter Datentyp beteiligt ist, vorzunehmen, muß das Schlüsselwort typedef verwendet werden. Mit tpedef wird ein neuer Bezeichner für einen bekannten Typ eingeführt:
16
int* ip = new int(0);
typedef float* fpt;
float* fp = fpt (ip);
In dem Beispiel wird der Pointer ip erzeugt, welcher auf den Integer Wert 0 im 'Heap' verweist.
Für den zusammengesetzten Datentyp float* wird der Bezeichner fpt eingeführt. Dies ermöglicht die anschließende Typkonversion.
Generell sei angemerkt, daß auf Typumwandlungen eher verzichtet werden soll. Man denke beispielsweise daran, daß durch die Typumwandlung Ungenauigkeiten entstehen können, wie dies
bei der Umwandlung von float in int-Werten der Fall sein kann.
2.5 Der Heap (die Operatoren New und Delete)
Aus den bisherigen Ausführungen zur Lebensdauer von Variablen ist klar geworden, daß diese
davon bestimmt wird, in welchem Bereich des Programms der Identifizierer deklariert wurde. Der
Programmierer hat allenfalls noch die Möglichkeit, die Lebensdauer durch Verwendung des
Schlüsselworts static zu beeinflussen. Eine generelle Kontrolle über die Lebensdauer der Variablen ist dem Programmierer aber hier nicht gegeben. Alle globalen Variablen existieren während
des gesamten Programmablaufs. Die lokalen Variablen werden im 'Stack' verwaltet.
Für Variablen, die im sogenannten 'Heap' verwaltet werden, hat der Programmierer die Möglichkeit, die Lebensdauer mit den beiden Schlüsselworten new und delete zu steuern. Die Erzeugung
einer Variable erfolgt mit dem Schlüsselwort new. Gelöscht wird eine Variable mit dem Schlüsselwort delete. Dies erfordert vom Programmierer aber auch eine aufmerksame Programmierung,
um keinen Speicherplatz mit nicht mehr benötigten Variablen zu verbrauchen. Sofern also der
delete Operator nicht auf eine Variable angewendet wird, welche vorher mit new erzeugt wurde,
bleibt der Speicherplatz während des Programmablaufs reserviert und wird erst nach der Beendigung des Programms vom Betriebssystem freigegeben. Daher sollte der Speicher für Variablen
auf dem 'Heap' freigegeben werden, wenn diese nicht mehr benötigt werden. Es ist bei Variablen,
welche auf dem 'Heap' abgelegt sind, auch darauf zu achten, daß diese noch erreichbar sind, da
ein Löschen natürlich sonst während des Programmablaufs auch nicht möglich ist. Wenn beispielsweise nur ein einziger Zeiger auf eine Variable im 'Heap' verweist und dieser Zeiger innerhalb eines Blocks definiert worden ist, so existiert der Zeiger nach Abarbeitung des Blocks nicht
mehr. Die Variable auf dem 'Heap' muß daher auch spätestens zum Blockende mit delete vom
Speicher entfernt werden. Sofern man die Variable aber noch benötigt, besteht sonst noch die
Möglichkeit, einen Zeiger auf die Variable zu setzen, der noch nach der Abarbeitung des Blocks
existiert.
Es ist zu beachten, daß der delete Operator nur auf Variablen angewendet wird, welche auch
tatsächlich im 'Heap' erzeugt wurden. Hat man beispielsweise einem Zeiger die Adresse einer
Variablen zugewiesen, die im 'Stack' abgelegt ist und wendet später den delete Operator an, um
die Variable, auf die der Zeiger verweist, freizugeben, kann dies zu einem Programmabsturz führen. Das Verhalten des Programms ist in diesem Fall jedenfalls unbestimmt und hängt vom verwendeten Compiler ab. Dies gilt auch, wenn versucht wird, den delete Operator anzuwenden,
obwohl noch gar kein Speicher mit new für einen entsprechenden Zeiger reserviert wurde oder
dieser mit delete schon vorher gelöscht wurde und dieser auch nicht auf 0 zeigt. Wenn der Zeiger
auf 0 zeigt, ist die Verwendung von delete dagegen wirkungslos.
Es kann natürlich auch der Fall eintreten, daß beim Versuch mit new dynamischen Speicher zu
reservieren, der vorhandene Speicherplatz nicht mehr ausreicht. In diesem Fall wird der betroffene
Zeiger auf 0 gesetzt. In dem folgenden Beispiel wird dieser Fall überprüft. Sofern er auftritt, wird
das Programm beendet und eine Fehlermeldung ausgegeben. Der Programmabbruch wird mit der
Funktion void exit(int i) herbeigeführt, welche über das 'Header-File' stdlib.h eingebunden
werden kann. Diese Funktion hat keinen Rückgabewert. Der Wert des Arguments i wird aber dem
Betriebssystem zur Verfügung gestellt. Wenn i den Wert 0 hat so, liegt eine fehlerfreie Beendigung
des Programms vor. Der Wert 1 verweist auf einen Fehler. Ob die erfolgreiche Zuweisung des
17
Speichers überprüft werden soll, hängt natürlich von der Anwendung ab. Wenn in dieser größere
Datenstrukturen verwaltet werden, ist diese Überprüfung sehr empfehlenswert.
Die Verwendung der Schlüsselwörter new und delete versetzt den Programmierer in die Lage, für
seine Anwendung eine optimale Speicherverwaltung zur Verfügung zu stellen. Weiter lassen sich
mit Hilfe des dynamischen Speichers dynamische Datenstrukturen wie beispielsweise dynamische
Arrays bereitstellen, da für solche Datenstrukturen der benötigte Speicherbedarf erst zur Laufzeit
ermittelt werden kann.
Es wird nun die Verwendung dieser Schlüsselwörter anhand einer Liste demonstriert, in welcher
Personen verwaltet werden:
#include <iostream.h>
#include <stdlib.h>
struct person
{
char* name;
char* vorname;
int
id;
person* next;
};
person* init (char* name, char* vorname,int id)
{
person* p = new person;
if(p==0)
{
cout<<"\n Der Speicherplatz reicht nicht mehr aus, um eine Variable
vom Typ person zu erzeugen. \n";
exit(1);
}
p->name = name;
p->vorname = vorname;
p->id = id;
p->next = 0;
return p;
}
void drucke_nachnamen(person*p)
{
person* h = p;
while(h)
{
cout<<h->name<<"\n";
18
h = h->next;
}
}
person* loesche_person(int id, person* p)
{
if (!p)
return p;
//Null-Liste
person *h1 = p;
person *h2 = p;
while (id != h1->id && h1->next !=0)
{
h2= h1;
h1 = h1->next;
}
if (h1->id == id && h1->id != p->id)
//Element mit gefundener id
//wird gelöscht
{
h2->next = h1->next;
delete h1;
return p;
}
else
if (id == p->id) // Sonderfall: Das erste Element der Liste
// wird gelöscht
{
h2=h1->next;
delete h1;
return h2;
}
else
return p; // es wurde kein Element gelöscht
}
person* remove_liste(person* p)
{
person* h;
while(p)
{
h = p;
19
p = p->next;
delete h;
}
return p;
}
void main(int argc, char * argv[])
{
person* p1 =
init("Mueller", "Hans", 1);
person* p2 =
init("Mayr", "Egon", 2);
person* p3 =
init("Wagner", "Karl", 3); // es wurden drei Personen
// angelegt
person* p;
p1->next = p2;
p2->next = p3;
p3->next =0;
// die drei Personen wurden in die Liste eingefügt
drucke_nachnamen(p1);
// die komplette Liste wird durchlaufen und die
// Nachnamen werden ausgegeben
p =
loesche_person(2,p1);
// das zweite Element der Liste wird
// gelöscht
drucke_nachnamen(p);
//delete p2;
// die neue Liste wird ausgegeben
führt zu einem Laufzeitfehler
p = remove_liste(p);
}
Bei diesem Programm werden die Eigenschaften einer Person, wie Name, Vorname und ID in
einer Struktur zusammengefaßt. Neue Personen werden mit der Funktion init dynamisch mit new
erzeugt. Anschließend werden die Personen zu einer Liste zusammengefügt. Die Liste wird durchlaufen und jeweils die Nachnamen mit drucke_nachnamen ausgegeben. Anschließend wird die
Person Mayr mit loesche_personen aus der Liste entfernt. Durch Ausgabe der neu entstandenen
Liste kann dies überprüft werden. Am Programmende werden alle verbleibenden Elemente auf
dem dynamischen Speicher mit der Funktion remove_liste gelöscht. Die Funktion remove_liste
dient hier lediglich zu Demonstrationszwecken für eine ordentliche Speicherverwaltung, da nach
Beendigung des Programms der reservierte Speicherbereich für den dynamischen Speicher auf
jeden Fall vom Betriebssystem freigegeben wird. Der Leser möge sich aber den Fall vorstellen,
daß nach Aufruf von remove_liste das Programm noch weiter läuft, ohne aber noch jemals auf
die Variablen in der erzeugten Liste zuzugreifen. In diesem Fall wäre der Aufruf von remove_liste erforderlich, um eine ordentliche Speicherverwaltung zu programmieren. Aus dieser
Demonstration wird ersichtlich, daß die Lebensdauer, der Objekte, welche mit dem 'Heap' verwaltet werden können, unter der Steuerung des Programmierers liegt.
Übungen:
1. Erweitern Sie die Funktionalität der Personenliste. Denken Sie beispielsweise an eine Funktion,
welche eine neue Person sortiert in die Liste einfügt.
20
3 Anweisungen
3.1 Verzweigungen
Wie bereits in der Personen-Liste kennengelernt, kann man Verzweigungen im Programmablauf
mit der if-Anweisung programmieren. Die Steuerung der if-Anweisung erfolgt über einen Ausdruck der auf true (1) oder false (0) getestet wird. Dies erfolgt häufig mit Hilfe von Vergleichsoperatoren:
==, // Test auf Gleichheit der beiden beteiligten Argumente
!=, // Test auf Ungleicheit der beiden beteiligten Argumente
<,
// Es wird überprüft, ob das linke Argument kleiner ist als das rechte
// Argument
<=, // Es wird überprüft, ob das linke Argument kleiner oder gleich dem rechten
// Argument ist
>,
// Es wird überprüft, ob das linke Argument größer ist als das rechte
// Argument
>=. // Es wird überprüft, ob das linke Argument größer oder gleich dem rechten
// Argument ist
Man beachte, daß der Test auf Gleichheit mit einem doppelten Gleichheitszeichen erfolgen muß.
Verwendet man lediglich ein einfaches Gleichheitszeichen, wird lediglich eine Zuweisung durchgeführt, dies kann u. U. zu Fehlern führen, welche nicht immer leicht entdeckt werden. Es folgen einige Beispiele:
int i = 0;
if (i)
cout<<i;
else
cout<<"Else-Teil"<<"\n";
Hier wird der else-Teil durchlaufen, da die Auswertung von i false ergibt.
i = 10;
if (i != 11)
{
cout<<i<<"\n";
cout<<"Hier werden mehrere Anweisungen in einem Block zusammengefaßt\n";
}
Soll im if- oder else-Block mehr als eine Anweisung ausgeführt werden, so müssen die Anweisungen durch geschweifte Klammern zu einem Block zusammengefaßt werden.
Ausdrücke können über logische Operatoren miteinander verbunden werden:
&&
Überprüft, ob beide beteiligten Argumente wahr sind
||
Überprüft, ob zumindest eines der beteiligten Argumente wahr ist
! Not-Operator, Liefert als Ergebnis das Gegenteil des auszuwertenden Arguments
Der 'Und-Operator' hat die Eigenschaft, daß die Auswertung abgebrochen wird, wenn die Auswertung des ersten Arguments false liefert, da dann der Gesamtausdruck auf keinen Fall mehr true
werden kann:
if (h1->id == id && h1->id != p->id)
21
{
h2->next = h1->next;
delete h1;
return p;
}
Wenn in diesem Fall h1->id == id false liefert, wird das zweite Argument nicht mehr ausgewertet.
Der 'Oder-Operator' beendet die Auswertung, wenn das erste Argument true liefert, da in diesem
Fall auch die Gesamtauswertung true ergeben wird.
Schließlich folgt noch ein Beispiel für den 'Not-Operator':
if (!false)
// tue irgendwas
Die Auswertung des Ausdrucks liefert immer true.
3.2 Der ?-Operator
Der '?-Operator' ist ein Konstrukt, mit welchem es möglich ist, manche Verzweigungen in einer
übersichtlicheren Form zu programmieren. Bei sehr komplexen Ausdrücken kann er jedoch unübersichtlich werden, so daß es sich in solchen Fällen eher anbietet, die if-Anweisung zu verwenden. Ein kleines Beispiel soll die Verwendung des if-Operators verdeutlichen:
#include <iostream.h>
void main(int argc, char * argv[])
{
int i,j, min;
i = 11;
j = 12;
min = i>=j ? j : i;
cout<<min<<"\n";
}
Mit dem '?-Operator' wird hier der kleinste der beiden zu überprüfenden Werte ermittelt, min zugewiesen und ausgegeben.
3.3 Die switch-Anweisung
Um starke Verschachtelungen mit if-Anweisungen zu vermeiden, kann man in einigen Fällen
eher die switch-Anweisung verwenden. Zurückkommend auf das Aufzählungsbeispiel, wäre der
folgende Einsatz der switch-Anweisung denkbar:
#include <iostream.h>
void main(int argc, char * argv[])
{
enum farben {rot, gruen, blau};
int i = 3;
while (farben(i))
{
22
i--;
switch (farben(i))
{
case rot:
cout <<"rot\n";
break;
case gruen:
cout <<"gruen\n";
break;
case blau:
cout<<"blau\n";
break;
default:
cout<<"Farbe unbekannt\n";
break;
}
}
}
Als Ausgabe erhält man:
blau
gruen
rot
Die Verwendung des Schlüsselworts break verhindert, daß bei einem Zutreffen eines case Labels
alle nachfolgenden Labels ebenfalls durchlaufen werden.
3.4 Goto
Mit goto wird ein Sprung von einer Stelle im Programm zu einer anderen durchgeführt. Die anzuspringende Stelle wird durch eine Marke gekennzeichnet. Die Verwendung von Sprüngen soll eher
vermieden werden, da ein Programm dadurch schnell unleserlich werden kann. In manchen Fällen
kann es jedoch sinnvoll sein, aus einer Schleife, if- oder switch-Anweisung herauszuspringen.
Betrachten wir dazu wieder ein Beispiel:
#include <iostream.h>
void main(int argc, char* argv[])
{
int i,j;
int counter = 0;
int search = 68;
int arr [10][10];
for (i = 0; i<10; i++)
{
23
cout<<"\n";
for (j = 0; j <10;j++)
{
arr [i][j] = counter++;
cout<<arr[i][j]<<"\t";
}
}
i = 0;
while (i++ < 10)
{
j = 0;
while (j++ < 10)
if (arr [i][j] == search)
goto exit;
}
exit :
if (arr[i][j] == search)
cout<<arr[i][j];
}
Die while-Schleifen werden verlassen, sobald das zu suchende Element im Array gefunden wurde. Wäre hier nicht die Möglichkeit gegeben, die while-Schleifen mit goto zu verlassen, müßte
man sich den entsprechenden Array-Index merken und die Schleifen würden bis zum Ende durchlaufen, was zusätzliche Rechenzeit erfordern würde, welche so eingespart werden kann.
4 Funktionen und Dateien
Funktionen werden eingesetzt, wenn geschriebener Programmcode mehrfach verwendet werden
soll. In solch einem Fall wird dieser Programmcode in den Rumpf einer Funktion eingegeben. Bei
Bedarf kann dann die Funktion ggf. unter Übergabe von Funktionsargumenten aufgerufen werden
und dann der Rumpf ausgeführt werden. In C++ liefern Funktionen Ihr Ergebnis mit dem Schlüsselwort return zurück. Wenn dieses return in der Funktion nicht vorkommt, dann wird die leere
Wertemenge void von der Funktion geliefert. Wenn eine Funktion kein Ergebnis mit return zurückliefert, dann zeigt sich die Wirkung beispielsweise dadurch, daß sie Ausgaben auf dem Bildschirm durchführt oder den Wert von globalen Variablen verändert.
Größere Programme werden auf mehrere Programmdateien aufgeteilt. Dabei enthalten diese Programmdateien Funktionen, welche von anderen Dateien aus aufgerufen werden. Die Verteilung
auf mehrere Dateien erhöht die Lesbarkeit. Weiter können Änderungen an einzelnen Dateien vorgenommen werden, ohne daß es notwendig wird, das komplette Programm neu zu übersetzen,
sondern lediglich die Datei, an welcher die Änderungen durchgeführt worden sind und diejenigen
Dateien, die diese Datei verwenden. Die zunächst einzeln übersetzten Dateien werden mit Hilfe
eines Linkers miteinander verbunden, dieser Linker ist Bestandteil des Entwicklungswerkzeugs.
4.1 Das Verbinden von Dateien
C++ stellt für das Aufteilen von Programmcode das Konzept der 'Headers-Dateien' bereit. In solchen 'Headers-Dateien' sollen lediglich die Funktionssignaturen und weitere Typen, Variablen und
Konstanten deklariert werden. Die eigentliche Implementierung der Funktion erfolgt in einer anderen Datei, welche keinen main-Teil besitzt. Schließlich kann eine weitere Datei (Client) programmiert werden, welche die Elemente der 'Header-Datei' in ihrem main-Teil nutzt. Die 'Header-Datei'
24
wird mit dem include-Befehl in die benutzenden Dateien eingebunden. Die Implementierungen
der 'Header-Datei' und des Clients werden zunächst getrennt kompiliert. Der bei der Kompilierung
entstehende Objektcode wird dann mit Hilfe eines Linkers zu einer Datei verbunden, welche dann
ausgeführt werden kann. Mit dieser Technik läßt sich also eine Client-Server Architektur umsetzen. Im folgenden soll anhand des bereits kennengelernten Personen-Beispiels das aufteilen des
Programmcodes auf mehrere Dateien demonstriert werden.
Es wird zunächst die hierzu erforderliche 'Header-Datei' aufgeführt. Für 'Headers-Dateien' wird in
der Regel die Endung h verwendet:
// Header-Datei: person.h
#include <iostream.h>
struct person
{
char* name;
char* vorname;
int
id;
person* next;
};
extern person* init (char* name, char* vorname,int id);
extern void drucke_nachnamen(person*p);
extern person* loesche_person(int id, person* p);
extern person* remove_liste(person* p);
Hier erfolgt in der 'Header-Datei' die Definition des Typs person. Die bereits kennengelernten
Funktionen init, drucke_nachnamen und loesche_person werden lediglich deklariert, so daß
hier das Schlüsselwort extern verwendet werden muß. Die eigentliche Definition der Funktionen
erfolgt in der Implementierung:
// Implementierung: person.cpp
#include <iostream.h>
#include "person.h"
person* init (char* name, char* vorname,int id)
{
person* p = new person;
p->name = name;
p->vorname = vorname;
25
p->id = id;
p->next = 0;
return p;
}
void drucke_nachnamen(person*p)
{
person* h = p;
while(h)
{
cout<<h->name<<"\n";
h = h->next;
}
}
person* loesche_person(int id, person* p)
{
if (!p)
return p;
person *h1 = p;
person *h2 = p;
while (id != h1->id && h1->next !=0)
{
h2= h1;
h1 = h1->next;
}
if (h1->id == id && h1->id != p->id) // Element mit gefundener id wird
// gelöscht
{
h2->next = h1->next;
delete h1;
return p;
}
else
if (id == p->id)
// Sonderfall: Das erste Element der Liste wird
// gelöscht
{
h2=h1->next;
delete h1;
return h2;
26
}
else
return p; // es wurde kein Element gelöscht
}
person* remove_liste(person* p)
{
person* h;
while(p)
{
h = p;
p = p->next;
delete h;
}
return p;
}
Die Implementierung definiert die in der 'Header-Datei' deklarierten Funktionen. Die 'Header-Datei'
wird mit dem include-Befehl eingebunden. Dies erfolgt jedoch unter Verwendung von Anführungszeichen. Dies geschieht im Unterschied zu 'Standard-Headers' wie beispielsweise iostream.h, welche in spitzen Klammern eingebunden werden. main wird in der Implementierung
nicht benötigt. Dies erfolgt in einer weiteren Datei, welche den Client repräsentiert und die von der
Implementierung bereitgestellten Funktionen benutzt:
// Client: main.cpp
#include <iostream.h>
#include "person.h"
void main(int argc, char * argv[])
{
person* p1 =
init("Mueller", "Hans", 1);
person* p2 =
init("Mayr", "Egon", 2);
person* p3 =
init("Wagner", "Karl", 3); // es wurden drei Personen
// angelegt
person* p;
p1->next = p2;
p2->next = p3;
p3->next =0;
// die drei Personen wurden in die Liste
// eingefügt
drucke_nachnamen(p1);
// die komplette Liste wird durchlaufen und die
// Nachnamen werden ausgegeben
27
p =
loesche_person(2,p1);
// das zweite Element der Liste wird
// gelöscht
drucke_nachnamen(p);
// die neue Liste wird ausgegeben
p = remove_liste(p);
}
main bindet ebenso wie die Implementierung die 'Header-Datei' person ein und benutzt die dort
deklarierten Funktionen und den Typ person.
Zunächst müssen nun die Implementierung und der Client getrennt übersetzt werden, was beispielsweise durch die folgenden Befehle auf dem Gnu-Compiler erfolgen kann:
$ c++ -c person.cpp
$ c++ -c main.cpp
Das Dollarzeichen ist der Systemprompt. Mit c++ wird auf meinem System der in der 'GnuCollection' enthaltene C++ Compiler aufgerufen. Alternativ kann man auch den Befehl gcc verwenden. Nach Ausführung dieser beiden Befehle liegt nun der Objektcode vor, welcher mit dem
Linker zu einer ausführbaren Datei verbunden werden kann:
c++ MyClient person.o main.o
Die daraus resultierende Datei MyClient kann dann ausgeführt werden.
$./MyClient
Damit wird das bereits kennengelernte Ergebnis ausgegeben.
Mit dem Aufteilen von Programmcode kann auch sehr gut das Geheimnisprinzip ('Information Hiding') durchgesetzt werden. Der Programmierer des Clients ist unabhängig von der internen Implementierung der Funktion. Diese braucht in nicht zu interessieren. Wenn der Programmierer des
Programmrumpfes Änderungen an diesem vornimmt, um beispielsweise die Algorithmen effizienter zu gestalten, hat dies keine Auswirkungen auf den Programmcode des Client. Er muß lediglich
neu kompiliert werden.
4.2 Bibliotheken
Mit Bibliotheken ist es möglich, das Verbinden von Dateien zu vereinfachen. Wenn der Client
Funktionen nutzt, welche über 'Headers-Dateien' (Schnittstellen) und zugehörigen Implementierungen zur Verfügung gestellt werden, ist es häufig recht aufwendig, diejenigen Objekt-Dateien
herauszusuchen, welche der Linker benötigt. Dies soll anhand der folgenden 'Header-Datei' mit
den dazugehörigen Implementierungen verdeutlicht werden:
//Header-Datei: mathfunk.h
#include <iostream.h>
extern float quadrat (float f);
extern float kubik (float f);
extern float div (float f, float g);
extern float invers (float f);
Diese 'Header-Datei' wird durch die Implementierungen quadrat.cpp, kubik.cpp, div.cpp und
invers.cpp implementiert:
// Implementierung: div.cpp
28
#include <iostream.h>
#include "mathfunk.h"
float div (float f, float g)
{
return (f/g);
}
// Implementierung: invers.cpp
#include <iostream.h>
#include "mathfunk.h"
float invers (float f)
{
return (div(1,f));
}
// Implementierung: quadrat.cpp
#include <iostream.h>
#include "mathfunk.h"
float quadrat (float f)
{
return (f*f);
}
// Implemetierung: kubik.cpp
#include <iostream.h>
#include "mathfunk.h"
float kubik (float f)
{
return (quadrat(f)*f);
}
Schließlich sei noch der Client angegeben, welcher die Funktionen kubik und invers benutzt:
// Client: main.cpp
#include <iostream.h>
#include "mathfunk.h"
void main(int argc, char * argv[])
{
float erg, arg1, arg2;
arg1 = 10.0;
arg2 = 20.0;
erg = kubik(arg1);
cout<<"Kubik von:\t"<<arg1<<"\t Ergebnis:\t"<<erg<<"\n";
29
erg = invers (arg1);
cout<<"Inversion von:\t"<<arg1<<"\t Ergebnis:\t"<<erg<<"\n";
}
Auf den ersten Blick sieht es so aus, als wenn es hinreichend wäre, wenn man den folgenden
Link-Vorgang durchführen würde:
$ -o MyClient main.o kubik.o invers.o
Dies wird aber nicht zum Erfolg führen, da in der Funktion invers die Funktion div und in der
Funktion kubik die Funktion quadrat benötigt wird.
Durch die Entwicklung von Bibliotheken kann diesem Problem begegnet werden. Dies passiert auf
meinem System durch den folgenden Befehl:
$ ar cr mathfunk.a quadrat.o div.o kubik.o invers.o
Anschließend kann main.cpp mit der Bibliothek mathfunk.a verbunden werden:
$ c++ main.cpp mathfunk.a
Mit dieser Vorgehensweise wird erreicht, daß unter Verwendung der Bibliothek nur diejenigen Objekt-Dateien eingebunden werden, welche jeweils von der main-Datei benötigt werden.
4.3 Funktionen
Innerhalb des Rumpfes einer Funktion wird Code ausgeführt, welcher an verschiedenen Stellen
vom Programm aufgerufen werden kann. Eine Funktion liefert an die aufrufende Stelle ein Ergebnis mit dem Schlüsselwort return zurück oder die leere Wertemenge void. Die Wirkung einer
Funktion ohne return kann darin bestehen, daß sie Ausgaben liefert, Geräte steuert oder die
Werte von globalen Variablen ändert.
4.3.1 Funktionen deklarieren
Bei der Funktionsdeklaration ist ein Name anzugeben und der Rückgabewert oder void zu spezifizieren, wenn kein return vorkommt. Weiter können noch Übergabeargumente mit Namen und
Typ angegeben werden, wobei bei einer Funktionsdeklaration die Namen der Übergabeargumente
nicht angegeben werden müssen:
extern int add(int i, int j);
// Funktion mit Rückgabewert int
extern float sub (float, float); // Funktion mit Rückgabewert float
extern void Print();
// Funktion ohne return
Ein Funktionsaufruf könnte sich dann so gestalten:
int erg = add(10,20);
4.3.2 Funktionen definieren
Bei einer Funktionsdefinition muß zusätzlich zur Funktionsdeklaration noch der Rumpf in geschweiften Klammern angegeben werden. Das Schlüsselwort extern wird dann natürlich nicht
verwendet:
int add (int i, int j)
{
return (i+j);
}
Durch Verwendung des Schlüsselworts inline wird erreicht, daß die gesamte Funktion beim
Kompilieren an der aufrufenden Stelle eingefügt wird. Damit kann bei kurzen Funktionen erreicht
werden, daß der Programmcode optimiert wird, da ein Funktionsaufruf während der Laufzeit wegfällt. inline sollte aber nur bei kurzen, vor allem einzeiligen Funktionen mit nicht mehr als zwei
30
Anweisungen verwendet werden, da bei umfangreichen Funktionen der Programmcode bei mehreren Aufrufen auch mehrmals kopiert werden muß.
inline int add (int i, int j) {return (i+j)};
4.3.3 Call by Reference and Call by Value
Werden die Argumente per 'Reference' an die Funktion übergeben, so hat dies zur Folge, daß die
Funktionsargumente als Alias für die übergebene Variable stehen. Beide verweisen auf denselben
Speicherplatz. Dies hat zur Folge, daß sich Werteänderungen in gleicher Weise auf die übergebene Variable auswirken. Diese Form des Aufrufs wird durchgeführt, indem einem Funktionsargument der Adreßoperator (&) vorangestellt wird. Bei einem Aufruf per 'Value' wird hingegen von der
aufrufenden Variablen eine Kopie angefertigt. Innerhalb der Funktion wird also nur mit der Kopie
gearbeitet. Änderungen, welche an der Kopie vorgenommen werden, haben somit keine Auswirkung auf die aufrufende Variable. Dies wird nun an einem Beispiel demonstriert, bei dem eine
Funktion verwendet werden soll, um die Werte der übergebenen Variablen der jeweils anderen
Variablen zuzuweisen. Die Funktion erhält die Variablen per 'Reference' und per 'Value' übergeben. Das Vertauschen funktioniert hier nur bei dem Aufruf per 'Reference':
#include <iostream.h>
void tausch_ByVal (int i, int j)
{
int temp;
temp = i;
i = j;
j = temp;
}
void tausch_ByRef (int &i, int &j)
{
int temp;
temp = i;
i = j;
j = temp;
}
void main(int argc, char * argv[])
{
int i = 10;
int j = 20;
cout<<i<<"\t"<<j<<"\n";
tausch_ByVal(i,j);
cout<<i<<"\t"<<j<<"\n";
tausch_ByRef(i,j);
cout<<i<<"\t"<<j<<"\n";
}
Das Programm liefert als Ergebnis:
10
20
10
20
31
20
10
In der ersten Zeile werden die ursprünglichen Variabelenwerte ausgegeben. Die zweite Zeile enthält die Variabelenwerte nach dem 'Call by Value' Aufruf. Nur die dritte Zeile liefert das gewünschte Ergebnis. Hier wurde ein 'Call By Reference' Aufruf durchgeführt.
Eine Referenzübergabe sollte nicht zu dem Zweck verwendet werden, um Variabelenwerte zu
ändern, da die Wartung von Programmen dadurch erschwert werden kann. Referenzübergaben
sollten eingesetzt werden, wenn Variablen übergeben werden, die sehr viel Speicher beanspruchen, da ein Kopieren dann ineffizient ist. Will man in solch einem Fall vermeiden, daß der übergebene Wert verändert wird, kann man das Schlüsselwort const verwenden:
int funk (const person &p);
Durch Verwendung von Zeigern kann man dieselbe Wirkung erreichen, wie bei einer Referenzübergabe:
#include <iostream.h>
void tausch_ByPoint (int* i, int* j)
{
int temp;
temp = *i;
*i = *j;
*j = temp;
}
void main(int argc, char * argv[])
{
int i = 10;
int j = 20;
cout<<i<<"\t"<<j<<"\n";
tausch_ByPoint(&i,&j);
cout<<i<<"\t"<<j<<"\n";
}
Auch mit diesem Programm wird das Vertauschen der Zahlen erreicht. Bei der Übergabe der Variablen, muß dann deren Adresse verwendet werden.
Arrays können grundsätzlich nicht per 'Value' übergeben werden. Wird ein Array an eine Funktion
übergeben, so wird hier ein 'Call by Reference' durchgeführt:
#include <iostream.h>
void arr_funk (char arr[][8])
{
char str [] = "Delete";
// in arr neu einzufügende Zeichenkette
for (int i=0; i<8;i++)
if (i<6)
arr[4][i] = str[i]; // Einfügen der Zeichenkette an die 5.
// Stelle von arr
else
arr[4][i] = '\0'; // die nicht verwendeten char Einträge
32
// an der 5. Stelle von arr werden mit dem
// Terminalzeichen aufgefüllt
}
void main(int argc, char * argv[])
{
char
str[] = "Eintrag";
char
arr[10][8];
for (int i = 0; i<10; i++)
for(int j = 0;j<8;j++)
arr[i][j] = str[j];
arr_funk (arr);
for (int i = 0; i<10; i++)
cout<<i<<". \t"<<arr[i]<<"\n";
}
Die Funktion arr_funk bekommt einen Array übergeben, welcher 10 Einträge vom Typ 'array of
char' enthält. In der Funktion wird der 5. Eintrag des übergebenen zweidimensionalen Arrays verändert. Die Funktion liefert lediglich die leere Wertemenge als Ergebnis zurück und das formale
Argument der Funktion weist keinen Referenzoperator auf. Nach dem Aufruf der Funktion wird der
Inhalt des übergebenen zweidimensionalen Arrays ausgegeben. An der 5. Stelle des Arrays wird
der in der Funktion veränderte Eintrag ausgegeben. Es wurde also tatsächlich ein 'Call by Reference' durchgeführt.
4.3.4 Überladen von Funktionen
Beim Überladen von Funktionen besteht die Möglichkeit, mehrere Funktionen mit unterschiedlichen Übergabeargumenten bei gleichem Namen zu definieren. Es wird dann jeweils die Funktion
aufgerufen, welche am besten zum jeweiligen Funktionsaufruf passt. Es ist allerdings nicht hinreichend, wenn die Funktion sich lediglich in den Rückgabeargumenten unterscheidet, da es in einem solchen Fall zu Mehrdeutigkeiten kommt, weil nicht entscheidbar ist, welche Funktion aufgerufen werden muß. Im folgenden wurde die Funktion drucke mehrfach überladen:
#include <iostream.h>
inline void drucke (int i)
{
cout<<"Ausgabe von Integer\t"<<i<<"\n";
}
inline void drucke (double d)
{
cout<<"Ausgabe von Double\t"<<d<<"\n";
}
inline void drucke (char ch)
{
cout<<"Ausgabe von Character\t"<<ch<<"\n";
33
}
void main(int argc, char * argv[])
{
int i = 10;
double d = 10.5;
float f = 10.56778f;
char ch = 'c';
drucke (i);
drucke (d);
drucke (ch);
drucke (f);
}
Je nachdem, welche Argumente der jeweilige Funktionsaufruf enthält, wird entschieden, welche
Funktion aufgerufen wird. Bis auf den Funktionsaufruf drucke (f) sind alle Funktionsaufrufe eindeutig zuzuordnen. Bei drucke(f) muß erst noch eine implizite Umwandlung von float nach double
erfolgen. Sofern ein Funktionsaufruf nicht zugeordnet werden kann, wird eine Fehlermeldung ausgegeben. Beim Überladen von Funktionen sollte man darauf achten, daß diese auch logisch zusammengehören.
4.3.5 Default-Argumente
Wenn es vorkommt, daß einer Funktion sehr häufig ein bestimmter Wert übergeben wird, dann
kann man dies mit Default-Werten lösen. Der Default-Wert wird im Funktionskopf eingesetzt und
immer dann verwendet, wenn kein anderer Wert angegeben wird:
#include <iostream.h>
inline void printer_status (const char* status = "Aufwaermphase")
{
cout <<status<<"\n";
}
void main(int argc, char * argv[])
{
printer_status();
printer_status("bereit");
}
Wenn printer_status ohne Argument aufgerufen wird, dann wird der Default-Wert verwendet,
was hier die Aufwärmphase ist. Bei Default-Argumenten muß beachtet werden, daß sie in der
Reihenfolge nach den nicht Default-Argumenten kommen müssen.
extern void funk (int j = 10, double d); // unzulässig
extern void funk (double d, int j = 10); // zulässig
4.3.6 Zeiger auf Funktionen
Wenn man einen Zeiger auf eine Funktion setzt, kann man über diesen die Funktion aufrufen. Bei
der Definition des Zeigers muß beachtet werden, daß diese zur Funktionssignatur passen muß:
#include <iostream.h>
34
inline void quadrat (int i)
{
cout<<i*i<<"\n";
}
void (*fp) (int);
void main(int argc, char * argv[])
{
fp = &quadrat;
(*fp) (10);
}
In dem Beispiel wird die Funktion quadrat über den Zeiger fp aufgerufen. Dies ist aufgrund der
passenden Definition des Zeigers möglich.
4.4 Makros
Mit Makros wird erreicht, daß Strings durch einen Präprozessor ersetzt werden. Durch Makros hat
der Programmierer die Möglichkeit seine eigene Sprache zu entwerfen, wodurch die Lesbarkeit
erheblich erschwert wird. Makros sollten daher nur sehr sparsam gebraucht werden. Im folgenden
ist ein Beispiel für die Verwendung von Makros aufgeführt. Hier werden die geschweiften Klammern durch die Wörter START und ENDE ersetzt.
#include <iostream.h>
#define START {
#define ENDE }
inline void quadrat (int i)
START
cout<<i*i<<"\n";
ENDE
void (*fp) (int);
void main(int argc, char * argv[])
START
fp = &quadrat;
(*fp) (10);
ENDE
5 Klassen
Mit Klassen wird die objektorientierte Komponente von C++ realisiert. Bei einem objektorientierten
Programm werden die Objekte des zu modellierenden Programms in den Mittelpunkt gestellt. Objekte werden durch Eigenschaften und Methoden beschrieben, wodurch ein Modell gebildet wird.
Eine Person wird beispielsweise durch die Eigenschaften Name, Vorname, Geburtsdatum usw.
beschrieben. Eine Methode könnte beispielsweise das Ausdrucken von Eigenschaften einer Person sein. Methoden und Eigenschaften werden in C++ zu einer Klasse zusammengefaßt. Mit Hilfe
dieser Klasse können dann Variablen erzeugt werden, welche die konkrete Ausprägung eines
Objekts repräsentieren. Klassen ermöglichen dem Programmierer den Zugriff auf Methoden und
die Spezifizierung von Eigenschaften. Im Sinne des Abstrakten Datentyps besteht ein Objekt daher aus einem öffentlichen und privaten Teil. Der Benutzer des Objekts hat über den öffentlichen,
ihm zugänglichen Teil, eine abstrakte aber hinreichende Sicht, um mit dem Objekt arbeiten zu
können. Die interne Repräsentation des Objekts ist für den Nutzer nicht wichtig und bleibt ihm da-
35
her verborgen. Damit soll ein einfacher Umgang mit dem Objekt sichergestellt werden und eine
unzulässige Verwendung, welche sich beispielsweise durch das Verwenden unzulässiger Werte
ergeben kann, vermieden werden. Durch den öffentlichen Tei kann sichergestellt werden, daß mit
den Daten des Objekts im intendierten Sinne gearbeitet wird. Hätte der Nutzer die Möglichkeit
direkt auf der internen Repräsentation zu arbeiten, wäre eine Kontrolle, wie sie durch die öffentliche Schnittstelle gewährleistet werden soll, nicht möglich.
Die objektorientierte Programmierung unterstützt somit die Entwicklung einer Client-ServerArchitektur. Der Server wird durch eine Klasse realisiert, welche die Dienste durch den Export der
öffentlichen Schnittstelle anbietet. Der Client nutzt in seinem Programm diese öffentliche Schnittstelle. Weiter kann durch die Trennung in privaten und öffentlichen Teil durch den Compiler die
korrekte Verwendung festgestellt werden. In einer prozeduralen Programmiersprache wie C oder
Pascal kann diese Arbeit vom Compiler nicht geleistet werden, so daß der Programmierer selbst
auf die entsprechenden Entwurfsrichtlinien achten muß.
5.1 Eigenschaften und Methoden
Die bereits kennengelernte Struktur (struct) person soll nun mit einer Klasse realisiert werden. In
dieser Klasse werden sowohl die Eigenschaften als auch die Methoden zusammengefaßt. Die
Eigenschaften von person werden in der Klasse als privater Teil deklariert. Die Methoden bilden
hier die öffentliche Schnittstelle, welche auf den internen Daten arbeitet:
#include <iostream.h>
class person
{
char* name;
char* vorname;
int
id;
person* next;
public:
void init (char* name, char* vorname,int id);
void drucke_nachnamen(person*p);
person*
loesche_person(int id, person* p);
person*
remove_liste(person* p);
void set_next(person* p);
};
void person::set_next(person* p)
{
next = p;
}
void person::init (char* name, char* vorname, int id)
{
this->name = name;
this->vorname = vorname;
36
this->id = id;
next = 0;
}
void person::drucke_nachnamen(person*p)
{
person* h = p;
while(h)
{
cout<<h->name<<"\n";
h = h->next;
}
}
person*
person::loesche_person(int id, person* p)
{
if (!p)
return p;
person *h1 = p;
person *h2 = p;
while (id != h1->id && h1->next !=0)
{
h2 = h1;
h1 = h1->next;
}
if (h1->id == id && h1->id != p->id) // Element mit gefundener id
// wird gelöscht
{
h2->next = h1->next;
delete h1;
return p;
}
else
if (id == p->id) // Sonderfall: Das erste Element der Liste wird
// gelöscht
{
h2=h1->next;
delete h1;
return h2;
}
else return p; // es wurde kein Element gelöscht
37
}
person* person::remove_liste(person* p)
{
person* h;
while(p)
{
h = p;
p = p->next;
delete h;
}
return p;
}
void main(int argc, char * argv[])
{
person* p1 = new person;
p1->init("Mueller", "Hans", 1);
person* p2 = new person;
p2->init("Mayr", "Egon", 2);
person* p3 = new person;
p3->init("Wagner", "Karl", 3); // es wurden drei Personen angelegt
person* p;
p1->set_next(p2);
p2->set_next(p3);
p3->set_next(0);
// die drei Personen wurden in die Liste eingefügt
p1->drucke_nachnamen(p1);
// die komplette Liste wird durchlaufen
// und die Nachnamen werden ausgegeben
p = p1-> loesche_person(2,p1);
// das zweite Element der Liste wird
//gelöscht
p->drucke_nachnamen(p);
// die neue Liste wird ausgegeben
p = p->remove_liste(p);
}
In der Klasse person sind nun die Eigenschaften und Methoden zusammengefaßt. Diese werden
als Klassen-Mitglieder ('Member') bezeichnet. Die Attribute bilden hier den privaten Teil. Dies bedeutet, daß ein Zugriff nur durch Klassen-Mitglieder erfolgen darf. Klassen-Mitglieder sind in einer
Klasse per Voreinstellung privat. Bei einer Struktur sind die Mitglieder dagegen per Voreinstellung
öffentlich. Will man in einer Klasse erreichen, daß ein oder mehrere Mitglieder öffentlich sein sollen, so muß hierzu das Schlüsselwort public gefolgt von einem Doppelpunkt in der Klasse angeführt werden. Es bietet sich daher bei einer Klasse an, zunächst diejenigen Mitglieder aufzuführen,
welche den privaten Teil bilden sollen. Will man Mitglieder explizit privat definieren, so ist das
Schlüsselwort private gefolgt von einem Doppelpunkt zu verwenden. Bei 'Methoden-Members'
kann man entweder die Funktion in der Klasse inline definieren oder lediglich den Funktionskopf
in die Klasse einfügen und dann außerhalb der Klasse die Methode definieren. In diesem Fall sind
38
nach dem Klassenname zwei Doppelpunkte gefolgt von dem Methodenname aufzuführen. Vor
dem Klassenname ist noch der Rückgabetyp anzugeben.
5.2 Der this-Zeiger
In der Methode init wird das Schlüsselwort this verwendet. Bei this handelt es sich um einen
Zeiger, welcher dasjenige Objekt referenziert, von dem der Aufruf der Klassen-Methode erfolgte.
Die Verwendung von this ist hier in init explizit erforderlich, da das Klassenattribut und das
Funktionsargument dieselbe Bezeichnung haben. Würde this nicht verwendet, so erfolgte eine
Zuweisung des Funktionsargumentes an sich selbst. Wären die beiden Namen hier unterschiedlich, so hätte this nicht verwendet werden brauchen. Bei this handelt es sich um eine Konstante, so daß this selber nicht verändert werden kann.
5.3 Get und Set Methoden
Im Code-Beispiel ist es nun nicht mehr möglich, direkt auf den privaten Teil von einer 'NotMember' Methode zuzugreifen. In main können daher nur die öffentlichen Methoden aufgerufen
werden, um Werte an die interne Repräsentation von der Klasse person weiterzuleiten. Das Code-Beispiel sollte daher noch um Methoden erweitert werden, die es erlauben, die Werte der internen Repräsentation zu setzen und abzurufen. Hierbei handelt es sich um Get- und Set-Methoden.
Bisher existiert lediglich eine Set-Methode für den next-Zeiger. In realen Anwendungen, müssen
aber auch die anderen Attribute gelesen und geschrieben werden, so daß eine Erweiterung des
Codes erforderlich wird:
#include <iostream.h>
class person
{
char* name;
char* vorname;
int
id;
person* next;
public:
void init (char* name, char* vorname,int id);
void drucke_nachnamen(person*p);
person*
loesche_person(int id, person* p);
void set_name(char* nachname);
void set_vorname(char* vorname);
void set_id (int id);
void set_next(person* p);
char* get_name();
char* get_vorname();
int get_id();
person* get_next();
};
void person::set_name(char* nachname)
{
name = nachname;
}
39
void person::set_vorname(char* vorname)
{
this->vorname = vorname;
}
void
person::set_id (int id)
{
this->id = id;
}
void person::set_next(person* p)
{
next = p;
}
char* person::get_name()
{
return name;
}
char* person::get_vorname()
{
return vorname;
}
int person::get_id()
{
return id;
}
person* person::get_next()
{
return next;
}
:
:
:
void main(int argc, char * argv[])
{
:
:
:
cout<<"\n"<<p1->get_name()<<"\t"<<p1->get_vorname()<<"\t"<<
p1->get_id()<<"\n";
person * hz = p1->get_next();
cout<<"\n"<<hz->get_name()<<"\t"<<hz->get_vorname()<<"\t"<<
hz->get_id()<<"\n";
40
p1->set_name("Schmidt");
p1->set_vorname("Else");
p1->set_id(1);
cout<<"\n"<<p1->get_name()<<"\t"<<p1->get_vorname()<<"\t"<<
p1->get_id()<<"\n";
:
:
:
}
Das Programm liefert dann für diese Aufrufe die folgende Ausgabe:
Mueller
Hans
1
Mayr
Egon
2
Schmidt
Else
1
Zunächst wird über die Get-Methoden das Objekt mit der ID 1 und dessen Nachfolger mit den GetMethoden ausgegeben. Anschließend erhält das Objekt mit der ID 1 mit Hilfe der Set-Methoden
neue Werte zugewiesen, welche dann mit den Get-Methoden ausgegeben werden.
In der Klassenmethode set_name() ist nun die Verwendung von this nicht notwendig, da sich
die Bezeichnung des Funktionsarguments (nachname) und die Bezeichnung des Klassenattributs
(name) unterscheiden.
Ein direkter Zugriff auf die privaten Klassenattribute ohne Verwendung der Get- oder SetMethoden ist hier nicht zulässig und wird vom Compiler zurückgewiesen:
cout<<p1->name;
Dadurch, daß der Client gezwungen ist, die Get- und Set-Methoden zur Bearbeitung der Klassenattribute zu verwenden, kann innerhalb solcher Klassenmethoden sichergestellt werden, daß nicht
vorgesehene Zugriffe überprüft und zurückgewiesen werden. Bei einem direkten Arbeiten auf den
Daten wäre dies nicht möglich. Beispielsweise wurde zwar in dem weiter oben angeführten Beispiel, in welchem das Objekt person mit einer Struktur realisiert wurde, auch nur mit den zur Verfügung gestellten Funktionen auf den Attributen gearbeitet. Die Attribute waren aber auch direkt
zugänglich, so daß hier eine sicherer Zugriff nicht gewährleistet werden kann.
5.4 Konstante Klassenmethoden und konstante Objekte
Klassenmethoden können mit dem Schlüsselwort const als konstant deklariert werden. Konstante
Klassenmethoden dürfen die Werte von Klassenmitgliedern nicht verändern. Eine Set-Methode
kann daher nicht konstant deklariert werden. Eine Get-Methode führt dagegen nur einen lesenden
Zugriff durch. Somit kann diese als konstant definiert werden.
Objekte, welche von Klassen erzeugt werden, können auch konstant deklariert werden. Solche
konstanten Objekte dürfen lediglich konstant deklarierte Klassenelemente aufrufen, da diese keine
Veränderung am Objekt vornehmen dürfen:
#include <iostream.h>
class pixel
{
int x;
int y;
int grauwert;
public:
41
void set_x(int x) {this->x = x;}
int get_x() const {return x;}
};
void main(int argc, char * argv[])
{
pixel p;
p.set_x(10);
const pixel cp = p;
p.set_x(20);
cout<<p.get_x()<<"\n";
cp.set_x(30); // unzulässiger Aufruf
cout<<cp.get_x()<<"\n";
}
In dem Beispiel sind lediglich zu Demonstrationszwecken die beiden Get- und Set-Methoden für
die X Koordinate eines Pixels aufgeführt. Die Get-Methode ist konstant deklariert und darf lediglich
lesend auf die Klassenelemente des Objekts zugreifen. Damit darf das in main definierte konstante Pixel cp nur die konstante Klassenmethode verwenden. Ein Aufruf der Set-Methode wird zurückgewiesen.
5.5 Konstruktoren
Um die Initialisierung für Klassen komfortabel durchführen zu können, kann man Konstruktoren
zur Verfügung stellen, welche die Initialisierung durchführen. Wenn vom Programmierer der Klasse kein Konstruktor zur Verfügung gestellt wird, verwendet das Programm implizit einen Standardkonstruktor, welcher bei der Definition des Objekts aufgerufen wird. Der Klassenname und der
Name des Konstruktors sind identisch. In der Signatur des Konstruktors kann der Programmierer
die für die Initialisierung notwendigen Argumente übergeben, welche dann im Rumpf des Konstruktors an die Klassenelemente weitergeleitet werden, welche initialisiert werden sollen. Auch
Konstruktoren können überladen werden, wie dies hier beispielhaft für die Klasse person durchgeführt wurde.
class person
{
:
:
public:
person(char* name, char* vorname,int id);
person();
:
:
:
};
:
:
42
person::person (char* name, char* vorname, int id)
{
this->name = name;
this->vorname = vorname;
this->id = id;
next = 0;
}
person::person ()
{
name = 0;
vorname = 0;
next = 0;
id = 0;
}
:
:
void main(int argc, char * argv[])
{
person* p1 = new person("Mueller", "Hans", 1);
person* p2 = new person();
// wird der explizite
person * p3 = new person;
// hier
Standard-Konstruktor aufgerufen
// dieser Aufruf ist dem
// vorherigen Aufruf gleichwertig
delete p1;
delete p2;
delete p3;
}
Im public Teil der Klasse wurden zwei Konstruktoren überladen. Der erste Konstruktor initialisiert
die drei Klassenmitglieder name, vorname und id. Sobald ein selbstdefinierter Konstruktor zur Verfügung gestellt wird, ist der implizite argumentlose Standardkonstruktor nicht mehr verfügbar. Aus
diesem Grunde wurde der selbstdefinierte Standardkonstruktor hier mit einem expliziten Standardkonstruktor überladen. Würde dieser selbstdefinierte Standardkonstruktor nicht zur Verfügung
stehen, wäre eine erfolgreiche Kompilierung nicht möglich.
In main sind schließlich Beispiele aufgeführt, in denen die Konstruktoren aufgerufen werden. Die
letzten beide Aufrufe sind gleichwertig. In beiden Fällen wird der explizit definierte Standardkonstruktor aufgerufen.
5.6 Destruktoren
Es wurde bereits angesprochen, daß mit dem Schlüsselwort new ein Objekt auf dem dynamischen
Speicher abgelegt werden kann. Die Speicherverwaltung bei solchen Objekten muß vom Programmierer durchgeführt werden. Objekte, die auf dem dynamischen Speicher liegen, werden mit
dem Schlüsselwort delete, wieder entfernt. delete erwartet als Argument einen Pointer oder den
Namen eines Arrays. Wenn mit delete ein Array gelöscht werden soll, müssen nach dem Schlüs-
43
selwort delete noch eine öffnende und eine schließende eckige Klammer angeführt werden.
Wenn in C++ ein Objekt gelöscht wird, dann erfolgt automatisch der Aufruf des sogenannten Destruktors. Der Destruktor beginnt mit einer Tilde (~) gefolgt vom Namen der Klasse. In solch einem
Destruktor sollen diejenigen Objekte mit delete gelöscht werden, die auf dem dynamischen Speicher liegen. Wird dies nicht getan und ein Objekt, welches weitere Objekte auf dem dynamischen
Speicher enthält, wird gelöscht, so belegen dessen Objekte, die auf dem dynamischen Speicher
abgelegt sind, weiter Speicherplatz, sind aber nicht mehr über den Objektnamen ansprechbar, da
dieser bereits gelöscht wurde. Wenn dann während des Programmslaufs sehr viele Objekte erzeugt und gelöscht werden, so kann es zu einer Erschöpfung des dynamischen Speichers kommen, so daß keine neuen Objekte mehr auf dem dynamischen Speicher erzeugt werden können.
In diesem Fall würde die Anwendung des new Operators keinen Speicher mehr zur Verfügung
stellen, sondern statt dessen den entsprechenden Pointer auf 0 setzen. Wenn dagegen sämtliche
Variablen des Objekts auf dem 'Stack' abgelegt werden, oder aber die Variablen global sind, ist
das Schreiben eines Destruktors nicht erforderlich. In den bisherigen Beispielen waren die Variablen der Objekte vom Typ person alle im 'Stack' abgelegt, so daß bisher kein Destruktor erforderlich war. Schauen wir uns an, wie bisher die Zuweisung an ein char* 'Member' erfolgte:
class person
{
char* vorname;
char* name;
:
:
};
:
:
person::person (char* name, char* vorname, int id)
{
this->name = name;
this->vorname = vorname;
:
:
}
:
void main(int argc, char * argv[])
{
person* p1 = new person("Mueller", "Hans", 1);
:
:
}
Durch diese Vorgehensweise, werden den beiden Variablen vorname und name ZeichenkettenKonstanten ('Pointer to char') zugewiesen, welche in diesem Beispiel im 'Stack' abgelegt sind.
Eine Speicherverwaltung durch den Programmierer ist daher nicht möglich, sondern ergibt sich
aus der Lebensdauer der Variablen bzw. Konstanten. Die beiden Variablen sind Zeiger, die hier
auf die Konstanten im 'Stack' verweisen. Eine tiefe Kopie hat hier nicht stattgefunden. Es liegt lediglich eine sogenannte flache Kopie vor. Wenn der Programmierer für diese beiden Objekte die
Speicherverwaltung übernehmen möchte, muß er die Variablen mit dem Befehl new auf dem
44
'Heap' ablegen und anschließend eine tiefe Kopie anfertigen. In diesem Fall kann der Konstruktor
folgendermaßen geschrieben werden:
person::person (const char* name, const char* vorname, int id)
{
if (name)
// Wenn name auf 0 zeigt, dann braucht
// kein Speicher bereitgestellt werden
{
this->name = new char [strlen(name)+1]; // es wird die notwendige
// Größe des dynamischen Speichers ermittelt
strcpy(this->name, name);
// zeichenweises Kopieren
}
else
this->name = 0;
if (vorname)
{
this->vorname = new char [strlen(vorname)+1];
strcpy(this->vorname, vorname);
}
else
this->vorname = 0;
this->id = id;
next = 0;
}
Mit diesem Konstruktor erfolgt nun die Verwaltung von name und vorname auf dem 'Heap'. Mit der
Bibliotheksfunktion strlen wird die Anzahl der Zeichen ermittelt, welche im übergebenen Argument enthalten sind. Dieser Wert wird um eins erhöht, da in C++ Strings mit dem Zeichen \0 beendet werden. Wenn das zuzuweisende Argument nicht auf 0 zeigt, wird mit new Platz auf dem
'Heap' in ausreichender Größe geschaffen und anschließend mit der Bibliotheksfunktion strcpy
jedes Zeichen des übergebenen Arguments in die Variable des Objekts kopiert. Es wurde somit
eine tiefe Kopie angefertigt. Diese beiden Bibliotheksfunktionen werden unter Visual C++ mit dem
'Header-File' string.h eingebunden. Bei dem Gnu-Compiler ist dies nicht erforderlich. Es folgt
nun das komplette Programm, bei der die Speicherverwaltung der beiden 'Klassenmember' auf
dem 'Heap' erfolgt. Das entfernen der Variablen vom 'Heap', wird hier vom Destruktor durchgeführt.
#include <iostream.h>
#include <string.h>
class person
{
char* name;
char* vorname;
45
int
id;
person* next;
public:
person();
person(const char* name, const char* vorname, int id);
void init (const char* name, const char* vorname,int id);
void drucke_nachnamen(person*p) const;
person*
loesche_person(int id, person* p) const;
person*
remove_liste(person* p) const;
void set_next(person* p);
void set_name(const char* nachname);
void set_vorname(const char* vorname);
~person();
};
person::person()
{
name = 0;
vorname = 0;
next = 0;
id = 0;
}
person::person (const char* name, const char* vorname, int id)
{
if (name)
// Wenn name auf 0 zeigt, dann braucht
// kein Speicher bereitgestellt werden
{
this->name = new char [strlen(name)+1]; // es wird die notwendige
// Größe des dynamischen Speichers ermittelt
strcpy(this->name, name);
// zeichenweises Kopieren
}
else
this->name = 0;
if (vorname)
{
this->vorname = new char [strlen(vorname)+1];
strcpy(this->vorname, vorname);
}
46
else
this->vorname = 0;
this->id = id;
next = 0;
}
void person::set_name(const char* nachname)
{
delete [] name;
if(nachname)
{
name = new char [strlen(nachname)+1];
// es wird die notwendige
// Größe des dynamischen Speichers ermittelt
strcpy(name, nachname);
//zeichenweises Kopieren
}
else
name = 0;
}
void person::set_vorname(const char* vorname)
{
delete [] this->vorname;
if (vorname)
{
this->vorname = new char [strlen(vorname)+1];
// es wird die
// notwendige Größe des dynamischen Speichers ermittelt
strcpy(this->vorname, vorname);
//zeichenweises Kopieren
}
else
this->vorname = 0;
}
void person::set_next(person* p)
{
next = p;
}
void person::init (const char* name, const char* vorname, int id)
47
{
delete [] this->name;
delete [] this->vorname;
if (name)
// Wenn name auf 0 zeigt,
// dann braucht kein Speicher bereitgestellt werden
{
this->name = new char [strlen(name)+1]; // es wird die notwendige
// Größe
des dynamischen Speichers ermittelt
strcpy(this->name, name);
// zeichenweises Kopieren
}
else
this->name = 0;
if (vorname)
{
this->vorname = new char [strlen(vorname)+1];
strcpy(this->vorname, vorname);
}
else
this->vorname = 0;
this->id = id;
next = 0;
}
void person::drucke_nachnamen(person*p) const
{
person* h = p;
while(h)
{
cout<<h->name<<"\n";
h = h->next;
}
}
:
:
:
person::~person() // die Definition des Destruktors
{
48
delete [] name;
delete [] vorname;
}
void main(int argc, char * argv[])
{
:
:
p = p->remove_liste(p);
// Aufruf des Destruktors
}
Für die Klasse Person wurde hier solch ein Destruktor geschrieben. Die Elemente name und vorname werden hier auf dem dynamischen Speicher abgelegt. Der Destruktor wird dann bei jedem
Löschen eines Objekts der Klasse person aufgerufen und der von name und vorname belegte dynamische Speicher wird freigegeben. Da name und vorname Arrays sind, müssen hier nach dem
Schlüsselwort delete noch die eckigen Klammern aufgerufen werden. Man darf auf keinen Fall
den delete Operator auf Variablen anwenden, welche im 'Stack' abgelegt sind:
char* name = "test";
delete [] name;
// Verhalten des Programms ist unbestimmt
Schließlich betrachten wir noch die Set-Methoden für name und vorname. Hier wird zuerst überprüft, ob der Zeiger bereits auf einen Wert verweist, um vor der neuen Zuweisung ggf. erst den
'Heap' vom alten Wert zu reinigen. Das Unterlassen dieser Überprüfung führt zu Speicherverschwendung, wenn der Zeiger noch auf einen alten Wert verweist, der dann mit delete nicht freigegeben wird.
5.7 Kopier-Konstruktoren
Der Kopier-Konstruktor kann bei der Definition und gleichzeitigen Initialisierung von Objektinstanzen eingesetzt werden, um beispielsweise zu erreichen, daß bei der Initialisierung eine tiefe Kopie
des zu übergebenden Objektes angefertigt wird. In dem folgenden Beispiel wird dies anhand der
Personenliste aus dem Universitäts-Informationssystem veranschaulicht:
#include <iostream.h>
#include <string.h>
class person
{
char* name;
char* vorname;
int
id;
person* next;
public:
person();
person(const person& p);
person(const char* name, const char* vorname, int id);
void init (const char* name, const char* vorname,int id);
49
void drucke_nachnamen(person*p) const;
person*
loesche_person(int id, person* p) const;
person*
remove_liste(person* liste, int long_remove = 0) const;
void set_next(person* p);
void set_name(const char* nachname);
void set_vorname(const char* vorname);
~person();
};
:
:
person::person(const person& p)
{
name = 0;
vorname = 0;
person const *h1 = &p;
person *h2;
person *h3;
if (h1 != 0)
{
set_name(p.name);
set_vorname(p.vorname);
id = p.id;
h1 = h1->next;
h3 = this;
}
else
{
id = 0;
next = 0;
}
while(h1 !=0)
{
h2 = new person(h1->name, h1->vorname, h1->id);
h3->next = h2;
h3 = h3->next;
h1=h1->next;
}
}
person* person::remove_liste(person* liste, int long_remove) const
{
person* h = liste ;
50
if (long_remove)
h = liste;
else
h = liste = liste->next;
while (h)
{
h = h->next;
delete liste;
liste = h;
}
return h;
}
person::~person()
{
delete [] name;
delete [] vorname;
}
void main(int argc, char * argv[])
{
person* p1 = new person("Mueller", "Hans", 1);
person* p2 = new person("Mayr", "Egon", 2);
person* p3 = new person("Wagner", "Karl", 3);
// es wurden drei Personen angelegt
person* p4 = new person();
// hier wird der Standard-Konstruktor aufgerufen
p1->set_next(p2);
p2->set_next(p3);
p3->set_next(0);
// die drei Personen wurden in die Liste
// eingefügt
cout<<"\n\n\n/********************************Ausgabe der Liste
p1************************************/\n";
p1->drucke_nachnamen(p1);
cout<<"/********************************Ende der Ausgabe von
p1************************************/\n\n\n";
person p = *p1;
// hier erfolgt der Aufruf des Kopierkonstruktors
p1 = p1->loesche_person(2,p1);
// das zweite Element der Liste wird
// gelöscht
cout<<"/********************************Ausgabe der Liste
p************************************/\n";
p.drucke_nachnamen(&p);
// die neue Liste wird ausgegeben
51
cout<<"/******************************** Ende der Ausgabe von
p************************************/\n\n\n";
cout<<"/********************************Ausgabe der
Liste p1************************************/\n";
p.drucke_nachnamen(p1);
cout<<"/********************************Ende der Ausgabe von
p1************************************/\n\n\n";
/***************************************Löschen der Variablen auf dem
Heap********************************/
p1->remove_liste(&p);
p1 = p1->remove_liste(p1,1);
/****************************************Programmende*********************
********************************/
}
Der Kopier-Konstruktor hat hier die Form person(const &person) und überlädt somit den Konstruktor. Sein Aufruf erfolgt nur bei der Definition und gleichzeitiger Initialisierung von Objektinstanzen. In diesem Fall erhält er die Adresse des zuzuweisenden Objekts als Argument übergeben. Wäre der Kopier-Konstruktor hier nicht definiert, so würde bei der Definition von Objektinstanzen vom Typ person lediglich eine Flache Kopie angefertigt, wenn, wie hier im Beispiel, eine
Personenliste zugewiesen würde. Dies erfolgt hier durch das erste Element der Liste (*p1), welches hier den Listenkopf repräsentiert. Bei einer flachen Kopie, würde hier lediglich eine Kopie von
dem ersten Element der Liste angefertigt, alle weiteren Elemente sind über Zeiger erreichbar, eine
komplette Kopie der Liste wird nicht stattfinden. Durch Programmierung des Kopier-Konstruktors
wird hier erreicht, daß die Personenliste komplett kopiert wird, so daß Änderungen, wie beispielsweise das Löschen von Listenelementen in einer Liste, keine Auswirkung auf die jeweils andere
Liste haben. Es ergibt sich daher die folgende Ausgabe:
/********************************Ausgabe der Liste
p1************************************/
Mueller
Mayr
Wagner
/********************************Ende der Ausgabe von
p1************************************/
/********************************Ausgabe der Liste
p************************************/
Mueller
Mayr
Wagner
/******************************** Ende der Ausgabe von
p************************************/
/********************************Ausgabe der Liste
p1************************************/
Mueller
Wagner
/********************************Ende der Ausgabe von
p1************************************/
52
Anhand dieser Ausgabe ist zu erkennen, daß die im Programm vorgenommene Löschung der
Person "Mayr" in der Liste, die über p1 zugreifbar ist, keine Auswirkungen auf die Liste hat, welche
über p erreichbar ist.
Wenn eine Definition und Initialisierung wie die folgende vorgenommen wird, ohne daß ein Kopierkonstruktor programmiert ist, wird lediglich eine flache Kopie der Personenliste durchgeführt.
person *p = p1;
Im folgenden ist die Ausgabe aufgeführt, die man dann erhält:
/********************************Ausgabe der Liste
p1************************************/
Mueller
Mayr
Wagner
/********************************Ende der Ausgabe von
p1************************************/
/********************************Ausgabe der Liste
p************************************/
Mueller
Wagner
/******************************** Ende der Ausgabe von
p************************************/
/********************************Ausgabe der Liste
p1************************************/
Mueller
Wagner
/********************************Ende der Ausgabe von
p1************************************/
Es ist nun zu erkennen, daß nach dem Löschen der Person "Mayr", dieses Listenelement auch
nicht mehr über p ausgegeben werden kann, so daß es sich bei der Liste, die über p und p1 zugreifbar ist, um dieselbe handelt.
5.8 Inline
Methoden können entweder direkt in einer Klasse definiert werden oder es erfolgt in der Klasse
lediglich die Deklaration. Wenn die Definition in einer Klasse erfolgt, dann wird die Methode vom
Compiler als inline behandelt. Dies bedeutet, daß kein Methodenaufruf durchgeführt wird, sondern der entsprechende Programmcode vom Compiler bereits an der richtigen Stelle im Maschinencode eingefügt wird, um keine Effizienzverluste durch Aufrufe zu erleiden. Wird eine Methode
dagegen außerhalb der Klasse definiert, so daß in der Klasse lediglich die Deklaration erfolgt,
dann hat dies zur Folge, daß durch die erforderlichen Methodenaufrufe Effizienzeinbußen hingenommen werden müssen. Dies kann allerdings vermieden werden, indem man das Schlüsselwort
inline verwendet. In diesem Fall wird der Code vom Compiler ebenfalls an die richtige Stelle im
Maschinencode eingefügt: Auch hier gilt dasselbe, was schon bei Funktionen gesagt wurde, daß
es sich nämlich nur dann lohnt eine Methode inline zu deklarieren, wenn diese recht kurz ist. Sie
sollte jedenfalls einzeilig sein und nicht mehr als zwei Anweisungen umfassen:
#include <iostream.h>
class person
{
char* name;
53
char* vorn;
int
id;
person* next;
public:
person(const char* name, const char* first_name,int id);
void set_id (int id);
:
inline void person::set_id (int id)
{
this->id = id;
}
:
:
Bei diesem Beispiel wurde die Methode set_id in der Klasse person lediglich deklariert. Die Definition erfolgte außerhalb der Klasse person. Durch Verwendung des Schlüsselworts inline wird
allerdings erreicht, daß set_id vom Compiler genauso behandelt wird, als wenn die Methode in
der Klasse definiert worden wäre.
5.9 Friends
Mit dem Konzept der 'Friends'-Funktionen wird eigentlich das Konzept des abstrakten Datentypen
wieder durchbrochen, da es sich hierbei um eine nicht 'Member'-Funktion handelt, welche aber auf
die privaten Elemente einer Klasse zugreifen darf. Aufgrund der bisherigen Ausführungen ergibt
sich daher, daß auf den Zugriff von friend-Funktionen eher verzichtet werden sollte. In einigen
Fällen kann der Einsatz von friend-Funktionen aber durchaus sinnvoll sein. Es wurde bereits
erwähnt, daß die öffentlichen 'Members'-Funktionen eingesetzt werden sollen, um sicherzustellen,
daß nur sinnvolle Operationen auf den privaten Daten durchgeführt werden. Dies kann aber beispielsweise bei komplexen iterativen Algorithmen, bei denen in jeder Iteration eine 'Member'Funktion aufgerufen wird, die rechenaufwendige Überprüfungen durchführt, Effizienzeinbußen zur
Folge haben.
In einer Klasse wird eine Funktion unter Verwendung ihres Funktionskopfes und der Verwendung
des Schlüsselworts friend bekannt gemacht. Hierbei kann es sich um eine Funktion oder aber
auch um eine Methode einer anderen Klasse handeln. Es folgt nun ein Codebeispiel, in welchem
das Programm mit der Klasse person um die Klasse lehrveranstaltung erweitert wurde. Hier
gibt es eine Funktion Buchung, die einen Zeiger auf eine Liste von Personen übergeben bekommt.
Diese Liste stellt die Teilnehmer der Lehrveranstaltung dar. Die Funktion Buchung greift auf private 'Members' beider Klassen zu und ist als friend in beiden Klassen deklariert:
#include <iostream.h>
#include <string.h>
class lehrveranstaltung;
class person
{
char* name;
char* vorn;
54
int
id;
person* next;
char* lv;
friend void buchung(lehrveranstaltung &lv, person* p);
public:
person(const char* name, const char* first_name,int id);
void drucke_nachnamen(person*p) const;
person*
loesche_person(int id, person* p) const;
void set_name(const char* nachname);
void set_vorname(const char* first_name);
void set_id (int id);
void set_next(person* p);
char* get_name() const;
char* get_vorname() const;
int get_id() const;
person* get_next() const;
void remove_liste() const;
~person();
};
:
:
class lehrveranstaltung
{
int lvnr;
char* lvname;
int teilnehmerzahl;
person* teil_liste;
friend void buchung(lehrveranstaltung &lv, person* p);
public:
lehrveranstaltung(int lvnr, const char* lvname, int teilnehmerzahl);
void drucke_lv();
void set_lvname(const char* lv);
~lehrveranstaltung();
};
lehrveranstaltung::lehrveranstaltung(int lvnr, const char* lvname, int
teilnehmerzahl)
{
55
teilnehmerzahl = 0;
teil_liste = 0;
this->lvname = 0;
this->lvnr = lvnr;
set_lvname(lvname);
}
void lehrveranstaltung::set_lvname(const char* lv)
{
delete [] lvname;
if (lv)
{
lvname = new char [strlen(lv)+1];
strcpy(lvname,lv);
}
else
lvname = 0;
}
void lehrveranstaltung::drucke_lv()
{
cout<<"\n LVName:"<<lvname<<"\n";
cout<<"\n LVNr:"<<lvnr<<"\n";
cout<<"\n Teilnehmeranzahl:"<<teilnehmerzahl<<"\n";
cout<<"\n Teilnehmerliste:"<<"\n";
while (teil_liste)
{
cout<<"\n"<<teil_liste->get_name()<<"\n";
teil_liste = teil_liste->get_next();
}
}
lehrveranstaltung::~lehrveranstaltung()
{
delete [] lvname;
}
void buchung(lehrveranstaltung &lv, person* p)
{
lv.teil_liste = p;
int counter = 0;
person* h = p;
56
while(h)
{
counter++;
// die Teilnehmeranzahl wird hier gezählt
p->lv = new char [strlen(lv.lvname)+1];
strcpy(p->lv,lv.lvname);
h = h->next;
}
lv.teilnehmerzahl = counter;
}
void main(int argc, char * argv[])
{
person* p1 = new person("Mueller", "Hans", 1);
person* p2 = new person("Mayr", "Egon", 2);
person* p3 = new person("Wagner", "Karl", 3);
// es wurden drei
//Personen angelegt
person* p;
p1->set_next(p2);
p2->set_next(p3);
// die drei Personen wurden in die Liste eingefügt
p1->drucke_nachnamen(p1);
// die komplette Liste wird durchlaufen
//und die Nachnamen werden ausgegeben
p = p1->loesche_person(2,p1);
// das zweite Element der Liste wird
// gelöscht
p->drucke_nachnamen(p);
// die neue Liste wird ausgegeben
cout<<"\n Test der Friend Funktion\n"<<"\n
---------------------------------\n";
lehrveranstaltung lv1(1,"C++",0);
buchung (lv1, p1);
lv1.drucke_lv();
//*******************************Loeschen des
Heap************************************************************
p1->remove_liste();
}
Die friend-Funktion buchung bekommt als Argumente sowohl eine Referenz auf lehrveranstaltung als auch einen Zeiger auf person übergeben. Die Deklaration von buchung in person
als friend, macht es daher erforderlich, daß der Compiler bereits beim ersten Auftreten der Klasse lehrveranstaltung diese kennen muß. Dies geschieht durch die Deklaration class lehrveranstaltung; wodurch lehrveranstaltung dem Compiler vorab bekannt gegeben wird.
Das Element teil_liste wird in Buchung mit einem Zeiger auf eine Personenliste initialisiert.
Damit wird die übergebene Personenliste zur Teilnehmerliste der Lehrveranstaltung, die an Bu-
57
chung übergeben wird. Die Teilnehmerliste wird dann in einer Schleife durchlaufen, um die Anzahl
der enthaltenen Teilnehmer zu ermitteln. Weiter steht in lehrveranstaltung eine Methode drukke_lv zur Verfügung, mit welcher die Daten der jeweiligen Lehrveranstaltung und deren Teilnehmer ausgegeben werden können.
5.10 Qualifizierter Zugriff auf Klassen-Elemente
Mitunter kommt es vor, daß Klassen-Elemente innerhalb einer Klasse durch andere Bezeichner
verdeckt werden. In diesem Fall kann auf die Klassen-Elemente ein qualifizierter Zugriff durchgeführt werden, indem dem Namen des Klassen-Elements der Klassenname gefolgt von einem
Doppelpunkt vorangestellt wird:
class punkt
{
int x;
int y;
public:
void set_x(int x)
{
punkt::x = x;
}
};
In dem Beispiel wird das Klassenelement x in der Set-Funktion durch das Argument x verdeckt.
Damit die Zuweisung an das Klassenelement erfolgreich durchgeführt werden kann, wird hier ein
qualifizierter Zugriff durchgeführt. Alternativ kann man diesen Zugriff auch mit dem this-Operator
durchführen. Diese Vorgehensweise wurde bereits weiter oben kennengelernt.
5.11 Eingebettete Klassen
Es besteht die Möglichkeit innerhalb eingebetteter Klassen weitere Klassen zu definieren, wodurch
die definierte eingebettete Klasse zur Element Klasse der umgebenden Klasse wird:
#include <iostream.h>
#include <string.h>
class lehrveranstaltung
{
int lvnr;
char* lvname;
class person
// eingebettete Klasse, die nur in lehrveranstaltung bekannt ist
{
char* name;
char* vorname;
int id;
58
char* lvname;
public:
person (const char* name, const char* vorname, int id);
void set_name(const char* nachname);
void set_vorname (const char* vorname);
void set_lvname(const char* lvname);
char* get_lvname() const;
~person();
};
public:
lehrveranstaltung (int lvnr, const char* lvname);
void set_lvname(const char* lvname);
void buchung(const char* name, const char* vorname, int id);
~lehrveranstaltung();
};
lehrveranstaltung::lehrveranstaltung (int lvnr, const char* lvname)
{
this->lvname = 0;
set_lvname(lvname);
this->lvnr = lvnr;
}
void lehrveranstaltung::set_lvname(const char* lvname)
{
delete [] this->lvname;
if(lvname)
{
this->lvname = new char [strlen(lvname)+1];
strcpy(this->lvname, lvname);
}
else
this->lvname = 0;
}
void lehrveranstaltung::buchung(const char* name, const char* vorname, int id)
59
{
person* p1 = new
person(name, vorname, id);
p1->set_lvname(lvname);
:
:
//
hier muß noch die Programmierung der
//
Teilnehmerliste eingefügt werden
}
lehrveranstaltung::~lehrveranstaltung()
{
delete [] lvname;
}
lehrveranstaltung::person::person (const char* name, const char* vorname, int
id)
{
this->name = 0;
this->vorname = 0;
lvname = 0;
set_name(name);
set_vorname(vorname);
this->id = id;
}
void lehrveranstaltung::person::set_name(const char* nachname)
{
delete [] name;
if (nachname)
{
name = new char [strlen(nachname)+1];
strcpy(name, nachname);
}
else
name = 0;
}
void lehrveranstaltung::person::set_vorname (const char* vorname)
{
60
delete [] this->vorname;
if (vorname)
{
this->vorname = new char [strlen(vorname)+1];
strcpy(this->vorname, vorname);
}
else
this->vorname = 0;
}
void lehrveranstaltung::person::set_lvname(const char* lvname)
{
delete [] this->lvname;
if (lvname)
{
this->lvname = new char [strlen(lvname)+1];
strcpy(this->lvname, lvname);
}
else
this->lvname = 0;
}
inline char* lehrveranstaltung::person::get_lvname() const
{
return lvname;
}
lehrveranstaltung::person::~person()
{
delete [] name;
delete [] vorname;
delete [] lvname;
}
void main(int argc, char * argv[])
{
61
lehrveranstaltung lv1(1,"C++");
lv1.buchung("Mayr","Egon", 1);// die Daten werden von Lehrveranstaltung an
//person weitergeleitet
}
Die Daten für die Klasse person werden dem Hauptprogramm nicht direkt übergeben. Dies erfolgt
durch Aufruf der Methode buchung, in der dann ein Zeiger auf ein Objekt vom Typ person neu
angelegt und mit den übergebenen Werten initialisiert wird. buchung muß noch weiter ausprogrammiert werden. Beispielsweise müßte die neu angelegte Person in eine eventuell vorhandene
Teilnehmerliste eingefügt werden.
Bei einer eingebetteten Klasse ist der direkte Zugriff ohne Verwendung der umgebenden Klasse
nicht möglich. Der folgende Aufruf würde daher vom Compiler zurückgewiesen:
person p;
Der Zugriff auf person ist daher außerhalb der umgebenden Klasse nur noch mit einem qualifizierten Zugriff über lehrveranstaltung möglich:
lehrveranstaltung::person p("Mayr", "Egon", 1);
5.12 Static-Elemente
Klassen-'Members' können static deklariert werden. Es ist dann möglich auf static-Member
außerhalb der Klasse ohne Verwendung eines Objekts zuzugreifen. Dies ist durchführbar, da auch
die Objekte keine Kopie von static-Elementen anlegen, sondern alle Objekte auf dasselbe Element zugreifen. Der Zugriff kann direkt über den Klassennamen gefolgt von zwei Doppelpunkten
durchgeführt werden:
#include <iostream.h>
class mathbib
{
public:
static float quadrat(float x)
{
return (x*x);
}
};
void main(int argc, char * argv[])
{
cout<<mathbib::quadrat(10);
}
Bei einer Bibliothek, welche beispielsweise Mathematik-Methoden zur Verfügung stellt, bietet sich
die Deklaration von static-Methoden an. Ein weiteres Einsatzgebiet für static-Elemente bietet
sich für einen Objektzähler an, der Auskunft darüber gibt, wieviel Objekte von einer Klasse erzeugt
wurden. Jedesmal, wenn ein neues Objekt erzeugt wird, erfolgt eine Inkrementierung des staticElements im Konstruktor.
5.13 Pointer auf Klassen-Member
Es wurde bereits vorgestellt, wie man Zeiger auf Funktionen programmieren kann. Die Programmierung von Zeigern auf Klassen-'Members' wird analog durchgeführt. Ein Zeiger auf ein Element
62
einer Klasse K wird durch K::* programmiert. Bei Methoden müssen dann noch die entsprechenden Argumente berücksichtigt werden:
#include <iostream.h>
class mathbib
{
public:
float quadrat (float x) const
{
return (x*x);
}
};
void main(int argc, char * argv[])
{
float (mathbib::*qm)(float) const;
// hier wird der Zeiger qm
// deklariert
qm
= &mathbib::quadrat;
// die Methode quadrat passt auf den Zeiger qm
mathbib* p1 = new mathbib;
cout<<(p1->*qm)(10.5f);
delete p1;
}
In dem Beispiel wird ein Zeiger auf die Klasse mathbib deklariert, der mit der Adresse von der
Methode quadrat initialisiert werden kann. Nach erfolgter Initialisierung kann die Methode unter
Angabe von Argumenten über den Zeiger aufgerufen werden.
6 Abgeleitete Klassen
Abgeleitete Klassen erweitern bestehende Klassen. Dies kann durch Einführung zusätzlicher Attribute bzw. Methoden erfolgen. Die erweiterte Klasse wird als Basis-, Super- oder Vaterklasse bezeichnet. Bei der abgeleiteten Klasse spricht man auch von der Kindklasse. Die Bezeichnung Vater- und Kindklasse deutet auf die Vererbung der Methoden und Attribute an die Kindklasse hin.
Da es sich bei einer abgeleiteten Klasse um eine Erweiterung der Basisklasse handelt, verfügt
diese auch über die Elemente der Basisklasse und ggf. über zusätzliche eigene Elemente. Die
Ableitung einer Klasse erfolgt, indem man nach dem neu eingeführten Klassennamen einen Doppelpunkt setzt und ggf. noch ein Ableitungsschlüsselwort (public, protected oder private) angibt. Wenn public angegeben wird, dann behalten alle Elemente der Basisklasse in der Kindklasse die gleiche Sichtbarkeit wie in der Basisklasse. Beispielsweise bleiben public-Elemente der
Vaterklasse in der Kindklasse ebenfalls public. Elemente, die mit der Sichtbarkeit protected
deklariert sind, können ausschließlich von der Kindklasse benutzt werden, wenn diese von der
Basisklasse mit dem Schlüsselwort public oder protected abgeleitet wurden. Wenn mit protected abgeleitet wird, dann erhalten die public-Elemente der Basisklasse ebenfalls die Sichtbarkeit
protected. Wird mit private abgeleitet, dann erhalten alle Elemente der Basisklasse die Sichtbarkeit private.
Innerhalb einer abgeleiteten Klasse wird der Konstruktor der Basisklasse im Konstruktor der Kindklasse aufgerufen, indem nach der Signatur des Konstruktors der Kindklasse ein Doppelpunkt
gefolgt von dem Konstruktoraufruf der Basisklasse aufgeführt wird. Hierbei handelt es sich um die
Initialisierungsliste der Klasse, in welcher auch die Klassenelemente initialisiert werden können,
anstatt die Zuweisungen im Rumpf des Konstruktors durchzuführen.
63
Wird ein Objekt einer abgeleiteten Klasse im Programm generiert, dann wird zunächst der Konstruktor der Basisklasse und dann der Konstruktor der abgeleiteten Klasse aufgerufen. Der Aufruf
bei mehreren Vererbungsebenen (also beispielsweise 'Fahrzeug<-Schienenfahrzeug<Strassenbahn' [der Pfeil bedeutet 'wird abgeleitet von']) erfolgt immer, indem mit der obersten Basisklasse begonnen wird und dann die Aufrufe der Konstruktoren entlang der Vererbung durchgeführt werden, bis die unterste abgeleitete Klasse erreicht wurde. Bei der Aufrufreihenfolge der Destruktoren wird dagegen immer mit dem Destruktor der untersten abgeleiten Klasse begonnnen
und entlang der Vererbungshierarchie, dann der jeweils übergeordnete Destruktor aufgerufen.
Die Programmierung soll an der bereits eingeführten Klasse person verdeutlicht werden. Man
denke an ein Informationssystem, welches in einer Universität eingesetzt werden soll. Aus diesem
Informationssystem will man u. a. Aufschluß über die Personengruppen an der Universität haben.
Bei den Personengruppen kann es sich insbesondere um Studenten und Personal handeln. In
beiden Fällen handelt es sich um Personen. Alle Klassenelemente, welche der Klasse person
angehören, treffen auch auf Studenten und Personal zu. Man muß aber zusätzlich beachten, daß
Personen und Personal noch weitere Elemente benötigen, damit sie sinnvoll in einem Informationssystem eingesetzt werden können. Jeder Student hat eine Matrikelnummer und jeder Bedienstete eine Personalnummer. Diese Beschreibung kann dann durch drei Klassen umgesetzt werden:
#include <iostream.h>
#include <string.h>
class lehrveranstaltung;
class person
{
char* name;
char* vorn;
int
id;
person* next;
char* lv;
friend void buchung(lehrveranstaltung &lv, person* p);
public:
person(const char* name, const char* first_name,int id);
void drucke_nachnamen(person*p) const;
void drucke_person() const;
person*
loesche_person(int id, person* p) const;
void set_name(const char* nachname);
void set_vorname(const char* first_name);
void set_id (int id);
void set_next(person* p);
char* get_name() const;
char* get_vorname() const;
int get_id() const;
person* get_next()const;
~person();
64
};
void person::drucke_person() const
// Ausgabe der Datenelemente von person
{
cout<<"\n Nachname:\t"<<name<<"\n";
cout<<"\n Vorname:\t"<<vorn<<"\n";
cout<<"\n PersonenID:\t"<<id<<"\n";
}
:
:
person::person (const char* nachname, const char* first_name, int id)
{
lv = 0;
if (nachname)
{
name = new char [strlen(nachname)+1];
strcpy(name, nachname);
}
else
name = 0;
if (first_name)
{
vorn = new char [strlen(first_name)+1];
strcpy(vorn, first_name);
}
else
vorn = 0;
this->id = id;
next = 0;
}
:
:
:
class student : public person
// student wird von der Klasse person abgeleitet
{
char* matnr;
char* studienfach;
public:
student(const char* matnr, const char* studium, const char* name, const
char* vorname, int id);
void drucke_student() const;
65
~student();
};
student::student(const char* matnr, const char* studium, const char* name, const
char* vorname, int id)
// der Konstruktor für student
: person(name, vorname , id)
// Aufruf des Konstruktors für person
{
if (matnr)
{
this->matnr = new char [strlen(matnr)+1];
strcpy(this->matnr, matnr);
}
else
this->matnr = 0;
if (studium)
{
studienfach = new char [strlen(studium)+1];
strcpy(studienfach, studium);
}
else
studienfach = 0;
}
void student::drucke_student() const
// Ausgabe von student
{
drucke_person();
// Ausgabe von person
cout<<"\n Mat.Nr.:\t"<<matnr<<"\n";
cout<<"\n Studienfach:\t"<<studienfach<<"\n";
}
student::~student()
{
delete [] matnr;
delete [] studienfach;
}
class personal : public person
// personal wird von person abgeleitet
66
{
char* persnr;
float gehalt;
public :
personal(const char* name, const char* vorname, int id, const char*
persnr, float gehalt);
void drucke_personal()const ;
~personal();
};
personal::personal(const char*name, const char* vorname, int id, const char*
persnr, float gehalt) // Konstruktor für personal
: person(name, vorname, id)
// Aufruf des Konstruktors für person
{
if (persnr)
{
this->persnr = new char [strlen(persnr)+1];
strcpy(this->persnr, persnr);
}
else
this->persnr = 0;
this->gehalt = gehalt;
}
void personal::drucke_personal() const
// Ausgabe von personal
{
drucke_person();
//Ausgabe von Person
cout<<"\n Pers.Nr.:\t"<<persnr<<"\n";
cout<<"\n Gehalt:\t"<<gehalt<<"\n";
}
personal::~personal()
{
delete [] persnr;
}
:
:
:
void main(int argc, char * argv[])
{
:
67
:
// Test der Klasse Personal
cout<<"\n/******************** Klasse Personal**************************/
\n";
personal* pl = new personal("Arbeiter", "Jupp", 4, "9999", 1500.47f);
pl->drucke_personal();
delete pl;
// Test der Klasse Student
cout<<"\n/******************** Klasse Student**************************/
\n";
student* st = new student( "11111", "Informatik", "Becker", "Hanna", 5);
st->drucke_student();
delete st;
}
Die beiden Klassen personal und student werden von der Klasse person public abgeleitet.
Beim Aufruf des Konstruktors der abgeleiteten Klasse, wird auch der Konstruktor der Basisklasse
aufgerufen, dabei werden die private-Elemente der Basisklasse initialisiert. Anschließend erfolgt
die Initialisierung der eigenen Daten-Elemente. In jeder Klasse existiert weiter eine Methode zur
Ausgabe der Attribute. In den abgeleiteten Klassen personal und student wird in der entsprechenden Methode zur Ausgabe der Attribute zunächst die drucke-Methode der Basisklasse person aufgerufen. Ein direkter Zugriff auf die Datenelemente der Basisklasse ist nicht möglich, da
diese in person private deklariert sind.
6.1 Redefinition von Methoden der Basisklasse
Im vorigen Beispiel sind die Methoden zur Ausgabe für die Klassen person, personal und student unterschiedlich benannt. Sollen aber beispielsweise alle Methoden zur Ausgabe lediglich mit
dem Bezeichner drucke benannt werden, so muß der Zugriff von einer abgeleiteten Klasse auf die
drucke-Methode der Basisklasse qualifiziert erfolgen, indem der Name der Basisklasse gefolgt
von zwei Doppelpunkten der aufzurufenden Methode der Basisklasse vorangestellt wird. Dieses
Vorgehen ist erforderlich, da bei gleichen Methodennamen die Methode der Basisklasse in der
abgeleiteten Klasse verdeckt wird:
#include <iostream.h>
#include <string.h>
class lehrveranstaltung;
class person
{
char* name;
char* vorn;
int
id;
person* next;
char* lv;
friend void buchung(lehrveranstaltung &lv, person* p);
public:
68
person(const char* name, const char* first_name,int id);
void drucke_nachnamen(person*p) const;
void drucke() const;
person* loesche_person(int id, person* p) const;
void set_name(const char* nachname);
void set_vorname(const char* first_name);
void set_id (int id);
void set_next(person* p);
char* get_name() const;
char* get_vorname() const;
int get_id() const;
person* get_next() const;
~person();
};
void person::drucke() const
{
cout<<"\n Nachname:\t"<<name<<"\n";
cout<<"\n Vorname:\t"<<vorn<<"\n";
cout<<"\n PersonenID:\t"<<id<<"\n";
}
:
:
void student::drucke() const
{
person::drucke();
// Zugriff auf die
// Methode der Basisklasse
cout<<"\n Mat.Nr.:\t"<<matnr<<"\n";
cout<<"\n Studienfach:\t"<<studienfach<<"\n";
}
:
:
void personal::drucke() const
{
person::drucke();
cout<<"\n Pers.Nr.:\t"<<persnr<<"\n";
// Zugriff auf die Methode
// der Basisklasse
cout<<"\n Gehalt:\t"<<gehalt<<"\n";
}
69
:
:
void main(int argc, char * argv[])
{
:
:
// Test der Klasse Personal
cout<<"\n/******************** Klasse Personal**************************/
\n";
personal* pl = new personal("Arbeiter", "Jupp", 4, "9999", 1500.47f);
pl->drucke();
delete pl;
// Test der Klasse Student
cout<<"\n/******************** Klasse Student**************************/
\n";
student* st = new student( "11111", "Informatik", "Becker", "Hanna", 5);
st->drucke();
delete st;
}
6.2 Virtuelle Methoden
Ein Pointer vom Typ person kann auch auf Objekte der Klassen student oder personal zeigen.
Dies ist möglich, da Objekte vom Typ personal oder student auch gleichzeitig Objekte der Klasse person sind. Damit besteht beispielsweise die Möglichkeit in einer Liste, in der eine Verwaltung
von Objekten des Typs person stattfinden soll, gleichzeitig auch Objekte vom Typ personal oder
student zu verwalten. Der umgekehrte Fall ist nicht möglich. Objekte vom Typ person sind nicht
unbedingt auch vom Typ personal oder student, so daß in einer Liste, in welcher beispielsweise
Objekte vom Typ student verwaltet werden, keine Objekte vom Typ person verwaltet werden
können.
Es wurde bisher in den Klassen person, student und personal eine Methode zur Ausgabe jeweils mit dem Namen drucke definiert.
Wenn nun ein Pointer vom Typ person auf ein Objekt vom Typ student zeigt und die Methode
drucke aufgerufen wird, so ist es nicht weiter verwunderlich, wenn die Methode drucke, die in
person definiert ist, aufgerufen wird, da ja der Zeiger von diesem Typ ist. Dies hat dann zur Folge,
daß die spezifischen Attribute von student nicht mit ausgegeben werden:
// Test eines Pointers vom Typ Person auf ein Objekt vom Typ Student
cout<<"\n/********************Pointer vom Typ
Person**************************/ \n";
person* p_st = new student( "22222", "Informatik", "Bauer", "Anita", 6);
p_st->drucke();
Es ergibt sich mithin:
/********************Pointer vom Typ Person**************************/
Nachname:
Bauer
Vorname:
Anita
PersonenID: 6
70
In dem Beispiel wird ein Zeiger vom Typ person definiert, welcher mit einem Objekt vom Typ student initialisiert wird. Anschließend wird über den Zeiger person die Methode drucke aufgerufen,
was zur Folge hat, daß lediglich die Attribute von person ausgegeben werden.
Der soeben beschriebene Effekt wird in den meisten Anwendungsfällen nicht weiter bringen, da
man auch eine Ausgabe der eigenen Attribute von student wünscht. Man kann dies erreichen,
wenn man die Methode drucke in der Basisklasse als virtuelle Funktion mit dem Schlüsselwort
virtual deklariert. Damit weiß der Compiler, daß es in den abgeleiteten Klassen weitere Versionen von drucke geben kann aber nicht geben muß. Es muß dann zur Laufzeit festgestellt werden,
welche Version von drucke aufgerufen werden muß. Dies hängt nun davon ab, auf welchen Typ
der Zeiger zur Laufzeit gesetzt ist.
Was passiert, wenn ein Pointer vom Typ person auf ein Objekt vom Typ student zeigt und dann
die Methode drucke aufgerufen wird, welche in der Basisklasse als virtual deklariert wurde?
Diese Objekte werden ja erst zur Laufzeit auf dem 'Heap' erzeugt. Dem Compiler kann also während des Kompilierens noch nicht bekannt sein, daß der Pointer vom Typ person während der
Laufzeit auf ein Objekt vom Typ student zeigen wird. Dieser Sachverhalt muß daher zur Laufzeit
festgestellt werden und wird als dynamisches Binden bezeichnet. Da zur Laufzeit der Typ ermittelt
wird, auf den der Pointer vom Typ student zeigt, ist es möglich, die Methode drucke von student aufzurufen.
Es folgt dazu nun ein kleines Beispiel. Die Klasse person wurde um eine statische Liste erweitert,
in welcher Personen verwaltet werden. Sofern in diese Liste tatsächlich nur Personen aufgenommen werden, gibt es keine Probleme bei der Ausgabe. In solch einer Liste können aber auch Objekte abgeleiteter Klassen aufgenommen werden. Wenn nun diese Liste durchlaufen wird und jeweils die Methode drucke aufgerufen wird, um die Attribute des jeweiligen Objekts auszugeben,
so muß in der Basisklasse drucke als virtuelle Funktion deklariert werden, wenn man erreichen
will, daß alle Attribute des jeweiligen Objekts ausgegeben werden:
#include <iostream.h>
#include <string.h>
class lehrveranstaltung;
class person
{
char* name;
char* vorn;
int
id;
person* next;
char* lv;
static person* liste;
// statische Liste, in welcher Objekte vom Typ
//"person" aufgenommen werden
friend void buchung(lehrveranstaltung &lv, person* p);
public:
person(const char* name, const char* first_name, int id);
void drucke_nachnamen(person*p) const;
virtual void drucke() const;
// virtuelle Deklaration
//von "drucke"
71
static void drucke_liste();
// statische Methode für
//die Ausgabe der Liste
static void remove_liste();
person* loesche_person(int id, person* p) const;
void set_name(const char* nachname);
void set_vorname(const char* first_name);
void set_id (int id);
void set_next(person* p);
static void set_liste(person* p);
char* get_name() const;
char* get_vorname() const;
int get_id() const;
person* get_next() const;
~person();
};
void person::drucke() const
// Definition der virtuellen Methode "drucke"
{
cout<<"\n Nachname:\t"<<name<<"\n";
cout<<"\n Vorname:\t"<<vorn<<"\n";
cout<<"\n PersonenID:\t"<<id<<"\n";
}
person* person::liste = 0; // die Definition der statischen
//Liste ist erforderlich
void person::drucke_liste()
{
person* h = liste;
while (h)
{
h->drucke();
h = h->next;
}
}
void person::remove_liste()
{
person* h;
while(liste)
{
h = liste;
liste = liste->next;
delete h;
72
}
}
inline void person::set_liste(person *p)
{
liste = p;
}
:
:
class student : public person
{
char* matnr;
char* studienfach;
public:
student(const char* matnr, const char* studium, const char* name, const
char* vorname, int id);
void drucke() const;
// die Version "drucke" für Objekte vom Typ
// "student"
~student();
};
:
:
void student::drucke() const
{
person::drucke();
cout<<"\n Mat.Nr.:\t"<<matnr<<"\n";
cout<<"\n Studienfach:\t"<<studienfach<<"\n";
}
class personal :public person
{
char* persnr;
float gehalt;
public :
personal(const char* name, const char* vorname, int id, const char*
persnr, float gehalt);
void drucke() const;
// die Version "drucke" für Objekte vom Typ
// "personal"
~personal();
};
:
:
73
void personal::drucke() const
{
person::drucke();
cout<<"\n Pers.Nr.:\t"<<persnr<<"\n";
cout<<"\n Gehalt:\t"<<gehalt<<"\n";
}
:
:
void main(int argc, char * argv[])
{
person* p1 = new person("Mueller", "Hans", 1);
person* p2 = new person("Mayr", "Egon", 2);
person* p3 = new person("Wagner", "Karl", 3);
personal* pl = new personal("Arbeiter", "Jupp", 4, "9999", 1500.47f);
student* st = new student( "11111", "Informatik", "Becker", "Hanna", 5);
p1->set_next(p2);
p2->set_next(p3);
p3->set_next(pl);
pl->set_next(st);
st->set_next(0);
person::set_liste(p1);
// die Personenliste enthält nun unterschiedliche Objekte
// Test der Personenliste mit unterschiedlichen Objekten
cout<<"\n/********************Test der Personenliste*****************************/ \n";
person::drucke_liste();
cout<<"\n/********************Ende der Personenliste*****************************/ \n";
person::remove_liste();
}
Es ergibt sich nun die folgende Ausgabe:
/********************Test der Personenliste*****************************/
Nachname:
Mueller
Vorname:
Hans
PersonenID: 1
Nachname:
Mayr
Vorname:
Egon
PersonenID: 2
Nachname:
Wagner
Vorname:
Karl
PersonenID: 3
74
Nachname:
Arbeiter
Vorname:
Jupp
PersonenID: 4
Pers.Nr.:
9999
Gehalt:
1500.47
Nachname:
Becker
Vorname:
Hanna
PersonenID: 5
Mat.Nr.:
11111
Studienfach:
Informatik
/********************Ende der Personenliste*****************************/
Die Ausgabe liefert das gewünschte Ergebnis. Beim Durchlaufen der Personenliste wird zur Laufzeit ermittelt, auf welchen Typ der Pointer gesetzt ist, so daß dessen Version der "drucke" Methode aufgerufen werden kann.
6.3 Abstrakte Klassen
In einigen Anwendungsfällen kann es sein, daß es keinen Sinn macht, für bestimmte Klassen Objektinstanzen im Programm zu erzeugen. Wenn man solche Klassen als abstrakte Klassen programmiert, hat dies zur Folge, daß von abstrakten Klassen keine Objektinstanzen erzeugt werden
dürfen. Eine Klasse ist dann abstrakt, wenn sie mindestens eine rein virtuelle Methode enthält.
Dies erreicht man dadurch, indem man einer virtuellen Methode die Null zuweist (=0). Soll eine,
von einer abstrakten Klasse, abgeleitete Klasse nicht virtuell sein, so muß sie alle rein virtuellen
Methoden der abstrakten Basisklasse überschreiben. Wenn nicht alle reinen virtuellen Methoden
der abstrakten Basisklasse überschrieben werden, dann ist die abgeleitete Klasse ihrerseits ebenfalls abstrakt und auch von ihr dürfen dann keine Objektinstanzen erzeugt werden. Abstrakte
Klassen sollen an dem Personen-Beispiel demonstriert werden. Es wurde bereits angesprochen,
daß die Klassen person, student und personal in einem Informationssystem für Universitäten
eingesetzt werden sollen. Eine Universität wird wohl nicht daran interessiert sein, Personen im
Sinne der Basisklasse zu verwalten. Es bietet sich daher an, die Klasse person als abstrakt zu
definieren. Dies wird über die Ausgabemethode drucke geschehen, welche als rein virtuell deklariert wird, wodurch erreicht wird, daß der Entwickler der Klasse student oder der Klasse personal gezwungen ist, für die entsprechende Klasse eine neue Version zur Verfügung zu stellen,
wenn von ihr Objektinstanzen erzeugt werden sollen:
#include <iostream.h>
#include <string.h>
class lehrveranstaltung;
class person
// abstrakte Basisklasse
{
char* name;
char* vorn;
int
id;
person* next;
char* lv;
static person* liste;
75
friend void buchung(lehrveranstaltung &lv, person* p);
public:
person(const char* name, const char* first_name,int id);
void drucke_nachnamen(person*p) const;
virtual void drucke() const = 0;
// rein virtuelle Methode
static void drucke_liste();
static void remove_liste();
person* loesche_person(int id, person* p) const;
void set_name(const char* nachname);
void set_vorname(const char* first_name);
void set_id (int id);
void set_next(person* p);
static void set_liste(person* p);
char* get_name() const;
char* get_vorname() const;
int get_id() const;
person* get_next() const;
~person();
};
:
:
class student :public
person
{
char* matnr;
char* studienfach;
public:
student(const char* matnr, const char* studium, const char* name, const
char* vorname, int id);
void drucke() const;
// Redefinition der rein virtuellen Methode
~student();
};
:
:
void main(int argc, char * argv[])
{
personal* pl = new personal("Arbeiter", "Jupp", 4, "9999", 1500.47f);
student* st = new student( "11111", "Informatik", "Becker", "Hanna", 5);
pl->set_next(st);
st->set_next(0);
person::set_liste(pl);
// Test der Personenliste mit unterschiedlichen Objekten
76
cout<<"\n/********************Test der Personenliste*****************************/ \n";
person::drucke_liste();
cout<<"\n/********************Ende der Personenliste*****************************/ \n";
person::remove_liste();
}
Es können in dem Programm nun keine Objektinstanzen vom Typ person mehr erzeugt werden:
person* p1 = new person("Mueller", "Hans", 1);
// Fehler
Diese Anweisung ist also nicht mehr zulässig. In der Personenliste können jetzt also nur noch
Studenten und Personal aufgenommen und über die redefinierte rein virtuelle Methode drucke
ausgegeben werden.
6.4 Mehrfache Vererbung
Das Informationssystem der Universität soll nun weiter ausgebaut werden, indem auch Tutoren
aufgenommen werden. Bei Tutoren handelt es sich um Studenten, welche einen Arbeitsvertrag mit
der Universität abgeschlossen haben und somit auch zum Personal der Universität gehören. Daraus ergibt sich, daß die zu programmierende Klasse tutor sowohl von student als auch von
personal abgeleitet werden muß. Dies geschieht, indem man die Basisklassen durch Komma
getrennt bei der Ableitung der Klasse angibt:
:
:
class tutor : public student, public personal
{
char* uebungsgruppe;
public:
tutor(const char* matnr, const char* studium, const char* name, const
char* vorname, int id, const char* persnr, float gehalt, const char*
uegruppe);
void drucke() const;
~tutor();
};
tutor::tutor (const char* matnr, const char* studium, const char* name, const
char* vorname, int id, const char* persnr, float gehalt, const char* uegruppe)
: student(matnr, studium, name, vorname, id), personal(name, vorname, id,
persnr, gehalt)
{
if (uegruppe)
{
uebungsgruppe = new char [strlen(uegruppe)+1];
strcpy(uebungsgruppe, uegruppe);
}
else
uebungsgruppe = 0;
77
}
void tutor::drucke() const
{
student::drucke();
personal::drucke();
cout<<"\n Übungsgruppe:\t"<<uebungsgruppe<<"\n";
}
:
:
Es kann hier allerdings zu Mehrdeutigkeiten kommen. Für jedes tutor-Objekt existiert jeweils ein
student-Objekt und ein personal-Objekt und jedes dieser Objekte hat ein eigenes Objekt vom
Typ person. Bei einem Aufruf von Elementen der Klasse person durch tutor ist nicht geklärt, ob
die Personen-Elemente von student oder von personal angesprochen sind, wodurch die Mehrdeutigkeiten ausgelöst werden. Um dieses Problem zu vermeiden, kann man bei der Ableitung
von student und person festlegen, daß es sich bei dessen Basisklasse person um eine virtuelle
Basisklasse handelt. Dadurch wird erreicht, daß bei der Ableitung von tutor die virtuelle Basisklasse person genau einmal existiert. Damit ist das Problem der Mehrdeutigkeit beseitigt:
class student : public virtual person
{
char* matnr;
char* studienfach;
public:
student(const char* matnr, const char* studium, const char* name, const
char* vorname, int id);
void drucke() const;
~student();
};
class personal : public virtual person
{
char* persnr;
float gehalt;
public :
personal(const char* name, const char* vorname, int id, const char*
persnr, float gehalt);
void drucke() const;
~personal();
};
:
:
tutor::tutor (const char* matnr, const char* studium, const char* name, const
char* vorname, int id, const char* persnr, float gehalt, const char* uegruppe)
: person(name ,vorname,id), student(matnr, studium, name, vorname, id),
personal(name, vorname, id, persnr, gehalt)
78
{
if (uegruppe)
{
uebungsgruppe = new char [strlen(uegruppe)+1];
strcpy(uebungsgruppe, uegruppe);
}
else
uebungsgruppe = 0;
}
:
:
Es ist hier zu beachten, daß in derjenigen Klasse, die von der virtuellen Basisklassen abgeleitet
wird, welche hier ja die Klasse tutor ist, der Konstruktor der Klasse aufgerufen wird, die die
Superklasse für die virtuellen Basisklassen ist. Dies ist hier der Konstruktor der Superklasse
person. Nun können die beschriebenen Mehrdeutigkeiten nicht mehr auftreten. Wenn man aber
die Ausgabe anschaut, wird man feststellen, daß für das Objekt vom Typ tutor die druckeAusgabe von person zweimal aufgerufen wird, nämlich einmal in student::drucke und einmal in
personal::drucke:
/********************Test der Personenliste*****************************/
Nachname:
Arbeiter
Vorname:
Jupp
PersonenID: 4
Pers.Nr.:
9999
Gehalt:
1500.47
Nachname:
Becker
Vorname:
Hanna
PersonenID: 5
Studienfach:
Informatik
Mat.Nr.:
11111
Nachname:
Mayr
Vorname:
Bettina
PersonenID: 6
Studienfach:
Informatik
Mat.Nr.:
22222
Nachname:
Mayr
Vorname:
Bettina
PersonenID: 6
Pers.Nr.:
8888
Gehalt:
547.67
Übungsgruppe:
Gruppe D, Freitag 10:00 Uhr
/********************Ende der Personenliste*****************************/
79
Dieses Problem kann man vermeiden, indem man in den Klassen person, student und personal
zwei Methoden zur Ausgabe einfügt. In einer der beiden Methoden erfolgt dann der Aufruf der
Ausgabe-Methode der übergeordneten Basisklasse, sofern eine solche existiert; weiter wird aber
auf jeden Fall von dieser Methode die zweite Ausgabe-Methode aufgerufen, in der die eigenen
Elemente ausgegeben werden. In tutor reicht dann lediglich eine Methode zur Ausgabe, in der
für jede Basisklasse diejenige Ausgabe-Methode aufgerufen wird, die lediglich die eigenen Elemente ausgibt. Dadurch wird erreicht, daß jedes Element nun noch genau einmal ausgegeben
wird:
#include <iostream.h>
#include <string.h>
class lehrveranstaltung;
class person
{
char* name;
char* vorn;
int
id;
person* next;
char* lv;
static person* liste;
friend void buchung(lehrveranstaltung &lv, person* p);
public:
person(const char* name, const char* first_name,int id);
void drucke_nachnamen(person*p) const;
void _drucke() const;
virtual void drucke() const = 0;
static void drucke_liste();
person* loesche_person(int id, person* p) const;
void set_name(const char* nachname);
void set_vorname(const char* first_name);
void set_id (int id);
void set_next(person* p);
static
void set_liste(person* p);
char* get_name() const;
char* get_vorname() const;
int get_id() const;
person* get_next() const;
~person();
};
person* person::liste = 0;
void person::_drucke() const
80
{
cout<<"\n Nachname:\t"<<name<<"\n";
cout<<"\n Vorname:\t"<<vorn<<"\n";
cout<<"\n PersonenID:\t"<<id<<"\n";
}
inline void person::drucke() const
{
_drucke();
}
:
:
class student : public virtual person
{
char* matnr;
char* studienfach;
public:
student(const char* matnr, const char* studium, const char* name, const
char* vorname, int id);
void _drucke() const;
void drucke() const;
~student();
};
:
:
inline void student::drucke() const
{
person::_drucke();
_drucke();
}
inline void student::_drucke() const
{
cout<<"\n Mat.Nr.:\t"<<matnr<<"\n";
cout<<"\n Studienfach:\t"<<studienfach<<"\n";
}
class personal :public virtual person
{
char* persnr;
float gehalt;
public :
personal(const char* name, const char* vorname, int id, const char*
persnr, float gehalt);
81
void _drucke() const;
void drucke() const;
~personal();
};
:
:
inline void personal::_drucke() const
{
cout<<"\n Pers.Nr.:\t"<<persnr<<"\n";
cout<<"\n Gehalt:\t"<<gehalt<<"\n";
}
inline void personal::drucke() const
{
person::drucke();
_drucke();
}
class tutor : public student, public personal
{
char* uebungsgruppe;
public:
tutor(const char* matnr, const char* studium, const char* name, const
char* vorname, int id, const char* persnr, float gehalt, const char* uegruppe);
void drucke() const;
~tutor();
};
tutor::tutor (const char* matnr, const char* studium, const char* name, const
char* vorname, int id, const char* persnr, float gehalt, const char* uegruppe)
: person(name ,vorname,id), student(matnr, studium, name, vorname, id),
personal(name, vorname, id, persnr, gehalt)
{
if (uegruppe)
{
uebungsgruppe = new char [strlen(uegruppe)+1];
strcpy(uebungsgruppe, uegruppe);
}
else
uebungsgruppe = 0;
}
void tutor::drucke() const
82
{
person::_drucke();
student::_drucke();
personal::_drucke();
cout<<"\n Übungsgruppe:\t"<<uebungsgruppe<<"\n";
}
:
:
Wenn man dieses Programm ausführt, erhält man die folgende Ausgabe:
/********************Test der Personenliste*****************************/
Nachname:
Arbeiter
Vorname:
Jupp
PersonenID: 4
Pers.Nr.:
9999
Gehalt:
1500.47
Nachname:
Becker
Vorname:
Hanna
PersonenID: 5
Mat.Nr.:
11111
Studienfach:Informatik
Nachname:
Mayr
Vorname:
Bettina
PersonenID: 6
Mat.Nr.:
22222
Studienfach:Informatik
Pers.Nr.:
8888
Gehalt:
547.67
Übungsgruppe:
Gruppe D, Freitag 10:00 Uhr
/********************Ende der Personenliste*****************************/
6.5 Zugriffskontrolle
Auf die Regeln, die bei der Zugriffskontrolle auf Elemente von Klassen gelten, wurde bereits eingegangen. Im Rahmen der Vererbung kommt dem Zugriffsspezifizierer protected eine Bedeutung zu, so daß protected-Elemente hier näher vorgestellt werden. Wenn in einer Klasse Elemente als protected deklariert werden, so hat dies zur Folge, daß auf diese Elemente nur die
Klasse selbst und von ihr abgeleitete Klassen zugreifen dürfen. Damit wird aber das Geheimnisprinzip wieder aufgebrochen. Es ist daher vorzuziehen, die zu schützenden Elemente einer Klasse
als private zu deklarieren und auch den abgeleiteten Klassen den Zugriff auf diese Elemente nur
über die öffentlichen Methoden der Klasse zu erlauben. Diese Vorgehensweise wurde bisher auch
bei den Demonstrationsbeispielen eingehalten, um die Funktionalität von protected Elementen
aufzuzeigen, wird nun anhand des Universitäts-Informationssystems der Zugriff auf protectedElemente demonstriert:
83
Die Ableitung einer Klasse erfolgt, indem man nach dem neu eingeführten Klassennamen einen
Doppelpunkt setzt und ggf. noch ein Ableitungsschlüsselwort (public, protected oder private)
angibt. Wenn public angegeben wird, dann behalten alle Elemente der Basisklasse in der Kindklasse die gleiche Sichtbarkeit wie in der Basisklasse. Beispielsweise bleiben public-Elemente
der Vaterklasse in der Kindklasse ebenfalls public. Elemente, die mit der Sichtbarkeit protected
deklariert sind, können ausschließlich von der Kindklasse benutzt werden, wenn diese in der Basisklasse mit dem Schlüsselwort public oder protected abgeleitet wurden. Wenn mit protected abgeleitet wird, dann erhalten die public-Elemente der Basisklasse ebenfalls die Sichtbarkeit
protected. Wird mit private abgeleitet, dann erhalten alle Elemente der Basisklasse die Sichtbarkeit private.
Das Informationssystem der Universität wird nun mit protected-Elementen ausgestattet:
:
class person
{
protected:
char* name;
char* vorn;
int
id;
person* next;
char* lv;
static person* liste;
friend void buchung(lehrveranstaltung &lv, person* p);
public:
:
};
:
class personal :public virtual person
{
protected:
char* persnr;
float gehalt;
public :
personal(const char* name, const char* vorname, int id, const char*
persnr, float gehalt);
void _drucke() const;
void drucke() const;
~personal();
};
:
void main(int argc, char * argv[])
{
personal* pl = new personal("Arbeiter", "Jupp", 4, "9999", 1500.47f);
pl->name = "Arbeiter";
// unzulässig
84
:
}
Da von der Klasse person weiter abgeleitet wird, bietet es sich an, die interne Repräsentation als
protected zu deklarieren. Die Klasse personal wurde von person abgeleitet und darf somit auf
die protected Elemente zugreifen, wie dies im Beispiel demonstriert ist. Außerhalb der Klasse
personal darf dagegen nicht auf solche Elemente zugegriffen werden. Daher schlägt hier die Zuweisung an name fehl und wird beim kompilieren mit dem Vermerk zurückgewiesen, daß es sich
um ein protected Element handelt, auf welches in diesem Kontext nicht zugegriffen werden darf.
Es wird nun ein weiteres Beispiel aufgeführt, an dem die Ableitung der Basisklasse mit unterschiedlichen Zugriffsspezifizierern vorgeführt wird:
#include <iostream.h>
class person
{
public: char* name;
};
class student :public person
{
public:
student(){name = 0;}
student(char* n) {name = n;}
};
class tutor : public student
{
public:
tutor(char* n): student() {name = n;}
};
void main(int argc, char * argv[])
{
tutor* t = new tutor("Jupp");
t -> name = "Hanna";
delete t;
}
Hier gibt es keine Probleme. Die Variable name ist in allen Klassen zugreifbar, da in person public abgeleitet wird. Dadurch wird erreicht, daß die Elemente der Basisklasse ihre Sichtbarkeit in
den abgeleiteten Klassen beibehalten. Da name in person public deklariert ist, ist sogar der
Zugriff von main aus möglich. Als nächstes wird der Fall untersucht, wenn person eine protected
Basisklasse für student ist:
#include <iostream.h>
class person
{
public:
char* name;
85
};
class student :protected person
{
public:
student(){name = 0;}
student(char* n) {name = n;}
};
class tutor : public student
{
public:
tutor(char* n): student() {name = n;}
};
void main(int argc, char * argv[])
{
tutor* t = new tutor("Jupp");
t -> name = "Hanna"; // unzulässiger Zugriff
delete t;
}
Für die Klasse student ist person nun eine protected Basisklasse, was zur Folge hat, daß die
public Variable in person für die abgeleiteten Klassen nur noch eine protected Sichtbarkeit hat.
Dies hat zur Folge, daß in main nicht mehr auf name zugegriffen werden kann. Der restriktivste Fall
ergibt sich, wenn person für student eine private Basisklasse wird:
#include <iostream.h>
class person
{
public: char* name;
};
class student :private person
{
public:
student(){name = 0;}
student(char* n) {name = n;}
};
class tutor : public student
{
public:
tutor(char* n): student() {name = n;} // unzulässiger Zugriff
};
void main(int argc, char * argv[])
{
86
tutor* t = new tutor("Jupp");
t -> name = "Hanna"; // unzulässiger Zugriff
delete t;
}
Nun liegen zwei unzulässige Zugriffe vor. Dadurch, daß person nun eine private Basisklasse für
student ist, werden alle Elemente von person in student zu private, was zur Folge hat, daß
auf diese private Elemente in tutor nicht mehr zugegriffen werden kann.
7 Operatoren überladen
Mit dem Konzept des Überladen von Operatoren besteht die Möglichkeit, die Funktionalität von
C++ Operatoren innerhalb von Klassen zu überladen. Dies kann hilfreich sein, wenn von der Klasse Objekte erzeugt werden können, auf denen ein solcher Operator sinnvoll angewendet werden
kann. Man denke beispielsweise an eine Klasse, mit der Arrays erzeugt werden können, die Vektoren repräsentieren. Auf solche Arrays lassen sich Rechenoperatoren wie +,* usw. anwenden.
Der Benutzer einer solchen Klasse wird sich dann wünschen, z.B. die Addition zweier solcher Arrays m und n in seinem Programm mit der Schreibweise sum = a+b vorzunehmen:
#include<iostream.h>
class array
{
static const int laenge = 5;
int arr[laenge];
public:
int get_laenge() const;
void set_array_value(int pos, int wert);
int get_array_value(int pos) const;
array operator+ (const array& n) const;
};
In der Klasse array wird ein int Array mit der konstanten Länge 5 deklariert. Es werden öffentliche Methoden zur Verfügung gestellt, mit welchen die Werte des Arrays gelesen und geschrieben
werden können. Wollte man nun in einer Programmiersprache, die das Konzept des Überladen
von Operatoren nicht unterstützt, wie z.B. Java, die Addition zweier Arrays programmieren, so
müßte man eine entsprechende Funktion bereitstellen, dessen Aufruf sich in der Form sum =
add(n,m) darstellen könnte, was nicht der mathematischen Schreibweise entspricht. In diesem
Beispiel ist daher für die Klasse array der Additionsoperator + überladen, was an Hand des
Schlüsselworts operator erkennbar ist. Im Hauptprogramm werden dann zwei Arrays initialisiert
und mit dem Befehl sum = n+m addiert und schließlich werden die Werte aller drei Arrays ausgegeben:
void main(int argc, char * argv[])
{
array n,m,sum;
for (int i = 0; i < n.get_laenge();i++)
{
n.set_array_value(i, 1+i);
m.set_array_value(i, 6+i);
87
}
sum = n+m;
//
sum = n.operator+(m); Diese Schreibweise ist äquivalent zu sum = n+m;
for (int i = 0; i < n.get_laenge(); i++)
{
cout <<i+1<<". Wert von n.arr["<<i<<"] = "<<
n.get_array_value(i)<<"\t";
cout <<i+1<<". Wert von m.arr["<<i<<"] = "<<
m.get_array_value(i)<<"\t";
cout <<i+1<<". Wert von sum.arr["<<i<<"] = "<<
sum.get_array_value(i)<<"\n";
}
}
Der Aufruf sum = n+m entspricht der Schreibweise sum = n.operator+(m). Die Methode operator+ wird also über das Objekt n aufgerufen und erhält als Argument den zweiten Operanden des
+ Operators übergeben. Somit stellt sich die überladene Operator Methode wie folgt dar:
array array::operator+ (const array& n) const
{
array sum;
for (int i=0; i < laenge; i++)
sum.arr[i] =
arr[i]+
n.arr[i];
return sum;
}
Das zu übergebende Argument sollte hier als konstant deklariert werden, da der Wert durch die
Methode operator+ nicht verändert werden soll. Objekte die Arrays beinhalten, können recht viel
Arbeitsspeicher beanspruchen, so daß sich ein 'Call by Reference' anbietet, damit keine Kopie des
übergebenen Arguments angefertigt wird und somit Speicherplatz eingespart werden kann. Der
Vollständigkeit halber sei noch die Implementierung der restlichen Methoden aufgeführt:
inline int array::get_laenge() const {return laenge;}
inline void array::set_array_value(int pos, int wert)
{
arr[pos] = wert;
}
inline int array::get_array_value(int pos) const
{
return arr[pos];
}
Es handelt sich hierbei um die Get- und Set-Methoden, mit denen auf die private Implementierung
zugegriffen werden kann.
Durch Einsatz des Konzepts des Überladens von Operatoren, kann ein Programm lesbarer werden, sofern man sich hierbei an allgemein bekannte Schreibweisen, wie beispielsweise die mathematische orientiert. Verwendet der Programmierer Operatoren in einem nur ihm verständlichen
Sinn, wird das Verwirrung stiften und ist daher zu unterlassen.
88
7.1 Möglichkeiten bei der Überladung von Operatoren
Es folgt nun eine Liste, in der die Operatoren aufgeführt sind, welche überladen werden können:
+
|=
>*
-
*
%=
<<
,
^=
%
^
&=
>>
->
>>=
[]
&
<<=
()
|
==
new
~
!
!=
=
<=
<
>=
>
+=
&&
||
-=
++
*=
--
/=
-
delete
7.2 Überladen des Zuweisungsoperators
Das Überladen des Zuweisungsoperators dient dazu, um bei der Zuweisung von Objektinstanzen,
die Funktionalität des Zuweisungsoperators neu zu programmieren. Dies kann z.B. seinen Einsatz
bei der Anfertigung von tiefen Kopien finden. Hierzu soll wiederum auf das Beispiel des Universitäts-Informationssystems zugegriffen werden, wobei die Verwaltung der Klasse person näher betrachtet wird. Man denke sich den Fall, daß nun eine solche Liste von Personeninstanzen existiert,
wobei nun der Listenkopf (ebenfalls ein Zeiger vom Typ person) an einen weiteren Zeiger vom
Typ person zur weiteren Bearbeitung zugewiesen werden soll. Es soll zunächst der Fall betrachtet
werden, bei dem die Zuweisung erfolgt, ohne das der Zuweisungsoperator überladen wird:
#include <iostream.h>
#include <string.h>
class person
{
char* name;
char* vorname;
int
id;
person* next;
public:
person(const char* name, const char* vorname,int id);
person();
void drucke_nachnamen(person*p) const;
person* loesche_person(int id, person* p) const;
person* remove_liste(person* liste, int long_remove = 0) const;
void set_name(const char* nachname);
void set_vorname(const char* vorname);
void set_id (int id);
void set_next(person* p);
char* get_name() const;
char* get_vorname() const;
int get_id() const;
person* get_next() const;
~person();
};
89
:
:
void main(int argc, char * argv[])
{
person* p1 = new person("Mueller", "Hans", 1);
person* p2 = new person("Mayr", "Egon", 2);
person* p3 = new person("Wagner", "Karl", 3);
// es wurden drei
// Personen angelegt
person* p4 = new person();
// hier wird der Standard-Konstruktor
// aufgerufen
person* p;
p1->set_next(p2);
p2->set_next(p3);
p3->set_next(0);
// die drei Personen wurden in die Liste eingefügt
cout<<"\n\n\n/********************************Ausgabe der Liste
p1************************************/\n";
p1->drucke_nachnamen(p1);
cout<<"/********************************Ende der Ausgabe von
p1************************************/\n\n\n";
p = p1;
p1 = p1->loesche_person(2,p1);
// das zweite Element der Liste wird
// gelöscht
cout<<"/********************************Ausgabe der Liste
p************************************/\n";
p->drucke_nachnamen(p);
// die neue Liste wird ausgegeben
cout<<"/******************************** Ende der Ausgabe
von p************************************/\n\n\n";
cout<<"/********************************Ausgabe der Liste
p1************************************/\n";
p->drucke_nachnamen(p1);
cout<<"/********************************Ende der Ausgabe von
p1************************************/\n\n\n";
p1 = p->remove_liste(p,1);
}
Man betrachte den Programmablauf in main. Zunächst wird in main eine Liste erzeugt, welche drei
Instanzen vom Typ person enthält. Von diesen Instanzen werden die drei Nachnamen 'Mueller',
'Mayr' und 'Wagner' ausgegeben. Bei dem Listenkopf handelt es sich um den Zeiger p1 vom Typ
person. Anschließend wird dem Zeiger p der Listenkopf p1 zugewiesen, wodurch eine sogenannte
flache Kopie erreicht wird. Diese ist deshalb flach, weil nun lediglich ein weiterer Zeiger auf die
gleiche Liste zeigt. Es findet keine Kopie der kompletten Liste statt. Wird nun mit Hilfe des Zeigers
von p1 ein Element aus der Liste gelöscht, so hat dies zur Folge, daß die Ausgabe der gelöschten
Objektinstanz über p auch nicht mehr möglich ist, da p ja auf dieselbe Liste wie p1 zeigt. Somit
erhält man folgende Ausgabe:
90
/********************************Ausgabe der Liste
p1************************************/
Mueller
Mayr
Wagner
/********************************Ende der Ausgabe von
p1************************************/
/********************************Ausgabe der Liste
p************************************/
Mueller
Wagner
/******************************** Ende der Ausgabe von
p************************************/
/********************************Ausgabe der Liste
p1************************************/
Mueller
Wagner
/********************************Ende der Ausgabe von
p1************************************/
Will man statt dessen eine tiefe Kopie der Liste anfertigen, so ist es erforderlich, den Zuweisungsoperator zu überladen. Mit einer tiefen Kopie wird erreicht, daß von der Personenliste eine vollständige Kopie angefertigt wird, so daß das Löschen in einer Liste jeweils keine Auswirkungen auf
die andere Liste hat. Um eine tiefe Kopie zu erhalten, wird der Zuweisungsoperator überladen und
seine Funktionalität im Rumpf der Methode so programmiert, daß dort die Kopie der Liste angefertigt wird und die Adresse des ersten Elements der neuen Liste zurückgeliefert wird. Diese Adresse
wird dann an *p (also an die Objektinstanz, auf die p zeigt) zugewiesen, so daß man über p auf die
neue Liste zugreifen kann:
#include <iostream.h>
#include <string.h>
class person
{
char* name;
char* vorname;
int
id;
person* next;
public:
person(const char* name, const char* vorname,int id);
person();
void drucke_nachnamen(person*p) const;
person*
loesche_person(int id, person* p) const;
void set_name(const char* nachname); //tiefe Kopie für name
void set_vorname(const char* vorname); //tiefe Kopie für vorname
void set_id (int id);
91
void set_next(person* p);
char* get_name() const;
char* get_vorname() const;
int get_id() const;
person* get_next() const;
person& operator=(const person& p);
person* remove_liste(person* p) const;
~person();
};
:
:
person& person::operator=(const person& p)
{
person const *h1 = &p;
person *h2;
person *h3;
h3 = this->next;
while (h3 !=0)
{
h2 = h3;
h3 = h3->next;
delete h2;
}
if (h1 != 0)
{
set_name(p.name);
set_vorname(p.vorname);
id = p.id;
h1 = h1->next;
h3 = this;
}
else
{
name = 0;
vorname = 0;
id = 0;
next = 0;
}
while(h1 !=0)
{
92
h2 = new person(h1->name, h1->vorname, h1->id);
h3->next = h2;
h3 = h3->next;
h1=h1->next;
}
return *this;
}
void main(int argc, char * argv[])
{
person* p1 = new person("Mueller", "Hans", 1);
person* p2 = new person("Mayr", "Egon", 2);
person* p3 = new person("Wagner", "Karl", 3);
// es wurden drei
// Personen angelegt
person* p4 = new person();
// hier wird der Standard-Konstruktor
// aufgerufen
person* p = new person;
p1->set_next(p2);
p2->set_next(p3);
p3->set_next(0);
// die drei Personen wurden in die Liste eingefügt
cout<<"\n\n\n/********************************Ausgabe der Liste
p1************************************/\n";
p1->drucke_nachnamen(p1);
cout<<"/********************************Ende der Ausgabe von
p1************************************/\n\n\n";
//*p = p->operator=(*p1); // Gleichwertiger Aufruf für die nachfolgende
//Zuweisung
*p = *p1;
// Aufruf des überladenen Zuweisungsoperators
p1 = p1->loesche_person(2,p1);
// das zweite Element der Liste wird
// gelöscht
cout<<"/********************************Ausgabe der Liste
p************************************/\n";
p->drucke_nachnamen(p);
// die neue Liste wird ausgegeben
cout<<"/******************************** Ende der Ausgabe von
p************************************/\n\n\n";
cout<<"/********************************Ausgabe der Liste
p1************************************/\n";
p->drucke_nachnamen(p1);
cout<<"/********************************Ende der Ausgabe von
p1************************************/\n\n\n";
p = p->remove_liste(p);
p1 =p1->remove_liste(p1);
}
93
Durch die Überladung des Zuweisungsoperators wird nun erreicht, daß beim Aufruf *p = *p1; die
Methode person& operator=(const person& p) aufgerufen wird. Im Prozedurkopf wird der
formale Parameter const person &p mit der Adresse des zuzuweisenden Elements *p1 initialisiert. Durch die Verwendung von const wird verhindert, daß *p1 selbst verändert wird. Da der
Aufruf als 'Call by Reference' erfolgt, wäre dies ja ansonsten möglich. Der Zeiger this verweist
auf das Element, an welchem die Zuweisung erfolgt. Dies ist hier die Objektinstanz *p. Da this
ggf. noch mit alten Werten initialisiert sein kann, ist es erforderlich, zu überprüfen, ob dies der Fall
ist, um ggf. diese alten Daten zu löschen. Anschließend kann damit begonnen werden die neue
Liste aufzubauen, wobei this auf das erste Element dieser neuen Liste verweist und somit den
Listenkopf darstellt. Sollte die zu kopierende Liste keine Elemente enthalten, so sind die Variablen
von this mit leeren Werten zu initialisieren. Nachdem die neue Liste angefertigt wurde, wird die
Adresse des Elementes, auf das this verweist, zurückgegeben, so daß *p diese Adresse
zugewiesen bekommt und somit auf den Listenanfang zeigt. Dadurch, daß *this zurückgeliefert
wird, sind auch verkettete Zuweisungen in der Form *p = *p1 = *p3 möglich. In dem Beispiel
existieren nun zwei eigenständige Listen. Das Löschen in der Liste auf die über p1 zugegriffen
wird, hat keine Auswirkungen auf diejenige Liste, auf die über p zugegriffen wird, so daß sich die
Ausgabe wie folgt darstellt:
/********************************Ausgabe der Liste
p1************************************/
Mueller
Mayr
Wagner
/********************************Ende der Ausgabe von
p1************************************/
/********************************Ausgabe der Liste
p************************************/
Mueller
Mayr
Wagner
/******************************** Ende der Ausgabe von
p************************************/
/********************************Ausgabe der Liste
p1************************************/
Mueller
Wagner
/********************************Ende der Ausgabe von
p1************************************/
Die Ausgabe läßt erkennen, daß nun zwei Listen existieren. Das Löschen des Elements in der
Liste auf die über p1 zugegriffen wird, hat keine Auswirkungen auf die Liste auf die über p zugegriffen wird. Auch nach dem Löschvorgang sind in dieser Liste noch alle drei Elemente enthalten.
7.3 Überladen des Ausgabeoperators ostream
Durch die Überladung des Ausgabeoperators ostream (<<) besteht die Möglichkeit, für Klasseninstanzen die Ausgabe neu zu programmieren. Im folgenden Beispiel wird die Klasse person hierfür herangezogen. In dieser Klasse wurde eine friend-Funktion deklariert, welche die Überladung
des Operators << vornimmt:
#include <iostream.h>
#include <string.h>
94
class person
{
char* name;
char* vorname;
int
id;
person* next;
public:
person(const char* name, const char* vorname,int id);
person();
void drucke_nachnamen(person*p) const;
person* loesche_person(int id, person* p) const;
void set_name(const char* nachname);
void set_vorname(const char* vorname);
void set_id (int id);
void set_next(person* p);
char* get_name() const;
char* get_vorname() const;
int get_id() const;
person* get_next() const;
person* remove_liste(person* p) const;
friend ostream& operator<< (ostream & os, const person& p);
~person();
};
inline ostream& operator<< (ostream & os, const person& p)
{
return os
<<"Id:\t"<<p.id<<"\n"<<"Name:\t"<<p.name<<"\n"<<"Vorname:\t"<<p.vorname<<"\n\n";
}
void main(int argc, char * argv[])
{
person* p1 = new person("Mueller", "Hans", 1);
person* p2 = new person("Mayr", "Egon", 2);
person* p3 = new person("Wagner", "Karl", 3);
// es wurden drei
// Personen
// angelegt
person* p4 = new person();
// hier wird der
// Standard-Konstruktor aufgerufen
person* p
= new person();
p1->set_next(p2);
p2->set_next(p3);
95
p3->set_next(0);
// die drei Personen wurden in die Liste eingefügt
cout<<*p1<<*p1->get_next();
p1 = p1->remove_liste(p1);
}
In main wird dann mit cout die friend-Funktion aufgerufen, wenn dieser als Argument eine Instanz vom Typ person übergeben wird.
7.4 Überladung des Array-Operators
Durch Überladen des Array-Operators kann man erreichen, daß auf Objektinstanzen mit diesem
Operator zugegriffen werden kann. Dies ist im folgenden an einem kleinen Beispiel aufgezeigt. Die
Klasse str beinhaltet ein private Element vom Typ 'Pointer to char'. Mit der public Methode
char& operator[](int) wird das Verhalten festgelegt, wenn auf eine Objektinstanz mit dem
Array-Operator und einem Index zugegriffen wird:
#include <iostream.h>
class str
{
char* c;
public:
str(char* ch) {c=ch;}
char& operator[](int i) const {return c[i];}
};
void main(int argc, char * argv[])
{
str s("C++");
for(int i = 0; i< 3;i++)
cout<<s[i]<<"\n";
}
Der Zugriff auf die Operator-Methode erfolgt hier von main aus. Als Resultat wird der Wert des
Arrays an der i. Position zurückgeliefert und hier mit cout ausgegeben.
8 Templates
Mit Klassen-'Templates' besteht die Möglichkeit die Objektklasse flexibler zu gestalten, da die
Möglichkeit besteht, anstelle von festen Typen lediglich Platzhalter bzw. Schablonen einzusetzen,
welche dann zur Laufzeit mit einem bestimmten Typ ausgefüllt werden. Man denke hier beispielsweise an die ID in der Klasse person welche als int deklariert war. Gehen wir davon aus, daß die
Objektinstanzen persistent in einer Datenbank abgespeichert werden sollen und dabei die ID im
Format float (bzw. einem dem Typ float entsprechenden kompatiblen Datentyp der verwendeten Datenbank) gespeichert werden sollen. In diesem Fall wäre die Adaption bei einem größeren
Programm mit einem gewissen Aufwand verbunden. Dies kann durch den Einsatz von 'Templates'
vermieden werden, da dann der Typ erst zur Laufzeit angegeben werden muß:
#include <iostream.h>
#include <string.h>
template <class temp>
96
class person
{
char* name;
char* vorname;
temp
id;
person* next;
public:
person(const char* name, const char* vorname,temp id);
person();
void drucke_nachnamen(person*p) const;
person<temp>*
loesche_person(temp id, person* p) const;
void set_name(const char* nachname);
void set_vorname(const char* vorname);
void set_id (temp id);
void set_next(person* p);
char* get_name() const;
char* get_vorname() const;
temp get_id() const;
person<temp>* get_next() const;
person(const person& p);
person<temp>* remove_liste(person* liste, int long_remove = 0) const;
~person();
};
:
:
Das Beispiel zeigt die Klasse person, welche in Verbindung mit dem 'Template' temp verwendet
wird. Da zur Laufzeit angegeben werden muß, mit welchem Typ das 'Template' ausgefüllt werden
soll, ist es erforderlich, die Schablone auch für diejenigen Klassenmethoden anzugeben, welche
als Rückgabewert den Typ 'Pointer to person' haben, wie dies hier in der Klassendeklaration geschehen ist.
Als nächstes wollen wir uns anschauen, wie die Definition der Klassenmethoden aussieht, wenn
diese nicht innerhalb der Klasse definiert werden:
:
:
template <class temp>
vorname, temp id)
person<temp>::person (const char* name, const char*
{
if (name)
{
this->name = new char[strlen(name)+1];
strcpy(this->name, name);
}
else
97
this->name = 0;
if (vorname)
{
this->vorname = new char[strlen(name)+1];
strcpy(this->vorname, vorname);
}
else
this->vorname = 0;
this->id = id;
next = 0;
}
template <class temp> person<temp>::person ()
{
name = 0;
vorname = 0;
next = 0;
id = 0;
}
template <class temp>void person<temp>::set_name(const char* nachname)
{
delete [] name;
if (nachname)
{
name = new char[strlen(nachname)+1];
strcpy(name, nachname);
}
else
name = 0;
}
template <class temp>void person<temp>::set_vorname(const char* vorname)
{
delete [] this->vorname;
if (vorname)
{
this->vorname = new char[strlen(vorname)+1];
strcpy(this->vorname, vorname);
}
else
this->vorname = 0;
98
}
template <class temp>inline void person<temp>::set_id (temp id)
{
this->id = id;
}
template <class temp>inline void person<temp>::set_next(person* p)
{
next = p;
}
template <class temp>inline char* person<temp>::get_name() const
{
return name;
}
template <class temp>inline char* person<temp>::get_vorname() const
{
return vorname;
}
template <class temp>inline temp person<temp>::get_id() const
{
return id;
}
template <class temp>inline person<temp>* person<temp>::get_next() const
{
return next;
}
template <class temp>void person<temp>::drucke_nachnamen(person*p) const
{
person<temp>* h = p;
while(h)
{
cout<<h->name<<"\n";
h = h->next;
}
}
template <class temp>person<temp>* person<temp>::loesche_person(temp id, person*
p) const
{
if (!p)
return p;
person<temp> *h1 = p;
99
person<temp> *h2 = p;
while (id != h1->id && h1->next !=0)
{
h2= h1;
h1 = h1->next;
}
if (h1->id == id && h1->id != p->id) // Element mit gefundener id wird
// gelöscht
{
h2->next = h1->next;
delete h1;
return p;
}
else
if (id == p->id) // Sonderfall: Das erste Element der Liste wird gelöscht
{
h2=h1->next;
delete h1;
return h2;
}
else
return p; // es wurde kein Element gelöscht
}
template <class temp> person<temp>::person(const person& p)
{
name = 0;
vorname = 0;
person<temp> const *h1 = &p;
person<temp> *h2;
person<temp> *h3;
if (h1 != 0)
{
set_name(p.name);
set_vorname(p.vorname);
id = p.id;
h1 = h1->next;
h3 = this;
}
else
{
100
next = 0;
id = 0;
}
while(h1 !=0)
{
h2 = new person(h1->name, h1->vorname, h1->id);
h3->next = h2;
h3 = h3->next;
h1=h1->next;
}
}
template <class temp>person<temp>* person<temp>::remove_liste(person* liste, int
long_remove) const
{
person<temp>* h = liste ;
if (long_remove)
h = liste;
else
h = liste = liste->next;
while (h)
{
h = h->next;
delete liste;
liste = h;
}
return h;
}
template <class temp> person<temp>::~person()
{
delete [] name;
delete [] vorname;
}
:
:
Vor jeder Klassenmethode muß auch wieder die Schablone mit dem entsprechenden Schablonennamen angegeben werden und entspricht damit ganz analog der Syntax, als wenn eine globale Funktion unabhängig von einer Klasse programmiert würde. In einem solchen Fall würde man
dann anstelle eines Klassen-'Templates' von einem Funktions-'Template' sprechen. Danach wird
101
der Rückgabewert angegeben. Das eigentliche Ausfüllen der Schablone geschieht dann zur Laufzeit in 'main' und erfolgt hier mit dem elementaren Typ int:
:
:
void main(int argc, char * argv[])
{
person<int>* p1 = new person<int>("Mueller", "Hans", 1);
person<int>* p2 = new person<int>("Mayr", "Egon", 2);
person<int>* p3 = new person<int>("Wagner", "Karl", 3);
// es wurden
// drei Personen angelegt
person<int>* p4 = new person<int>();
// hier wird
// der Standard-Konstruktor aufgerufen
p1->set_next(p2);
p2->set_next(p3);
p3->set_next(0);
// die drei Personen wurden in die Liste eingefügt
cout<<"\n\n\n/********************************Ausgabe der Liste
p1************************************/\n";
p1->drucke_nachnamen(p1);
cout<<"/********************************Ende der Ausgabe von
p1************************************/\n\n\n";
person<int> p = person<int>(*p1);
p1 = p1->loesche_person(2,p1);
// das zweite Element der Liste wird
// gelöscht
cout<<"/********************************Ausgabe der Liste
p************************************/\n";
p.drucke_nachnamen(&p);
// die neue Liste wird ausgegeben
cout<<"/******************************** Ende der Ausgabe von
p************************************/\n\n\n";
cout<<"/********************************Ausgabe der Liste
p1************************************/\n";
p.drucke_nachnamen(p1);
cout<<"/********************************Ende der Ausgabe von
p1************************************/\n\n\n";
p.remove_liste(&p);
p1 = p1->remove_liste(p1,1);
}
Wenn nun eine Objektinstanz erzeugt werden soll, dessen ID im Format float abgespeichert werden soll, ist dies leicht zu realisieren, indem man einfach die Schablone mit dem Schlüsselwort
float ausfüllt:
person<float>* p1 = new person<float>("Mueller", "Hans", 1.0F);
Der Abschluß zum Thema 'Templates' soll durch ein Beispiel gebildet werden, in dem von einem
Array Gebrauch gemacht wird, welches in einer Klasse als private Element definiert ist und dessen Größe über einen 'Template'-Parameter bestimmt wird:
102
#include <iostream.h>
template <class temp, int index>
class arr
{
temp a[index];
public:
};
void main(int argc, char * argv[])
{
arr<int,10> a;
}
Hier wird die Schablone mit zwei Werten gefüllt. Der erste Wert bestimmt die Datentypen des Arrays, der zweite Wert ist vom Typ int und legt den Umfang des Arrays fest.
103
Herunterladen