ã 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