Klassen und ähnliche Elemente und Strukturen in C++ (auch im Vergleich zu Java) Ein Vortrag von Marc Meyer am 25.7.2000 Übersicht: Vorab Klassen Kapselung Datenelemente Methoden this- Zeiger Instanzenbildung Konstruktor/ Destruktor Zugriffe - innerhalb der Klasse - von außerhalb der Klasse - auf Basisklassenelemente Abstrakte Klassen und Methoden Vererbung Vererbung, Mehrfachvererbung Polymorphie, virtuelle Basisklassen Lokale und verschachtelte Klassen Nicht mehr in diesem Vortrag (für die Vollständigkeit) Bibliotheken Strukturen/ Unions Namespaces Vorab Objektorientiertes Programmieren ist in Java eng mit dem Begriff der Klassen verbunden. Hier werden Objekte mit ähnlichen Merkmalen und Verhaltensweisen zusammengefasst. Das bringt eine gewisse Übersichtlichkeit, spart aber vor allem eine Menge Programmierarbeit, man kann sich nach Absprache der Schnittstellen die Arbeit mit anderen Programmierern teilen, und hilft Programme zu Modularisieren. Nicht anders verhält sich die Sache bei der Programmiersprache C++. Dieser Vortrag soll einen kleinen Einblick in das OOP mit C++ geben und auch die Gemeinsamkeiten und Unterschiede zu Java aufzeigen, stellt aber keinesfalls den Anspruch auf Vollständigkeit, was angesichts der Fülle der Möglichkeiten, im positiven wie auch negativen Sinne, nicht möglich ist. Wer diesen Vortrag hört, oder auch selbst liest, sollte bereits etwas Ahnung vom Aufbau der Klassen in Java oder C++ haben, da der Vortrag zwar in sich geschlossen ist und einem roten Faden folgt, aber hier und dort schon auf Begriffe vorgreift, welche im einzelnen erst später erläutert werden (z.B. sollte der geneigte Hörer/ Leser mit dem Begriff eines Konstruktors etwas anzufangen wissen). Klassen Prinzipieller Aufbau einer Klasse in C++: Klassentyp Klassenname [:Basisklasse(n)] { Elementenliste } [Instanzen]; Klassentyp: - Schlüsselwörter class, struct und union (zu letzteren später mehr) - Meist (sogar empfohlen) nur class in Verwendung Klassenname: - Name der zu deklarierenden Klasse nach den gültigen Konventionen [:Basisklassen]: - durch Kommata getrennte Liste der vererbenden Klassen - Es besteht die Möglichkeit, durch Zugriffsspezifizierer (public, protected und private) den Zugriff auf ererbte Elemente zu regulieren. - Notwendigkeit der vorherigen Deklaration der Basisklassen vor Vererbung Elementenliste: - Deklaration und u.U. auch Definition von Datenelementen und Methoden - Regelung des Zugriffs durch Spezifizierer (public, protected und private) - Geltungsbereich eines Zugriffsspezifizierers bis zum Auftreten des Nächsten - mehrfaches Auftauchen eines Spezifizierers möglich - Gemeinsamer Gültigkeitsbereich aller Elemente einer Klasse: Ihre Klasse. - Klassenelemente am Anfang der Deklaration ohne Zugriffsspezifizierer, gelten automatisch als private! Instanzen: - optionale Möglichkeit der Einrichtung von Instanzen (Variablen) der Klasse, bei mehreren Instanzen getrennt durch Kommata Beispiel: class simplesBeispiel { int wert; // private public: int zweiterWert = 4; simplesBeispiel ( ) //Konstruktor, dazu später mehr { wert=1+2; } protected: int wertAusgabe( ) { return(wert + zweiterWert); } private: int loesche_Wert ( ); }; Was fällt auf im Vergleich zu Java? - kein Modifizierer für die Klasse (also z.B. keine final class) - Mehrfachvererbung möglich, dafür keine Implementierung von Schnittstellen - Aufteilung der Elementenliste in Spezifiziererbereiche (bei Java jedes Element mit seinem eigenen Zugriffsspezifizierer) - sofortige Instanzenbildung - ein Semikolon am Ende der Klasse Zur Erinnerung hier noch einmal das Prinzip einer Klasse in Java: [Zugriffsspezifizierer] class NameDerSuperklasse] [implements NameDerSchnittstelle(n)] { Elementenliste; } Klassenname [extends Die Zugriffsspezifizierer bei der Klassendeklaration in Java: - public - final - abstract - ohne: friendly Kapselung Die Zusammenfassung von Datenelementen und Methoden wird als Kapselung bezeichnet. Zudem verbinden sich mit dem Begriff noch zwei weitere Designkriterien: - Information hiding - Abgeschlossenheit Information hiding: Die Klassen sollten hauptsächlich über ihre Methoden angesprochen werden, sie bilden die Schnittstellen zwischen der Klasse und dem Programm. Durch die Zugriffsspezifizierer wird die Schnittstelle genauer bezeichnet. Es ist nun ratsam, wichtige Elemente (sie sollten besser als private deklariert werden) der Klasse nicht direkt ansprechen zu lassen, sondern in vorgegebenem Maße über public Elemente, Funktionen. Abgeschlossenheit: Die Schnittstellen einer Klasse sollten möglichst vollständig sein, was wäre (zugegeben etwas sehr bildlich gesprochen) eine Fernbedienung für einen Videorekorder ohne Play- Taste? Es sollten in der Klasse aber auch nur Funktionen enthalten sein, die die Objekte dieser Klasse wirklich kennzeichnen. Um wieder sehr bildlich zu werden: was soll die Videorekorderfernbedienung mit einem Knopf für eine elektronisches Garagentor? Dies sollte im übrigen nicht nur für das Programmieren in C++ gelten, sondern auch für Java. Datenelemente Variablen, die innerhalb einer Klasse definiert werden, stellen die Datenelemente der Klasse dar. Es sind ebenso wie in Java instanzenspezifische Variablen, d.h., für jede Instanz werden Kopien dieser Variablen angefertigt (Ausnahme kommt gleich!). Für Datenelemente gilt: - sie können als public, protected oder private deklariert werden, - werden mit der Instanzenbildung eingerichtet und mit dieser auch wieder aufgelöst - und können auch als static, const, volatile oder mutable deklariert werden. Nun zur Ausnahme: statische Datenelemente. Sie werden bei der Instanzenbildung nicht kopiert, es greifen alle Instanzen auf die gleiche Variable zu (das ist genauso wie in Java!). Weiterhin ist es notwendig, statische Elemente außerhalb der Klasse zu definieren (definieren, nicht deklarieren!), etwa im umgebenden namespace oder Dateibereich. Das steht im krassen Gegensatz zu Java, wo auch statische Datenelemente im Klassenbreich definiert werden. In C++ können sie nicht nur über die Instanzen sondern auch über den Klassennamen angesprochen werden. class Demo { public: static int instanzenzaehler; //Deklaration Demo ( ) {instanzenzaehler++;}; //Konstruktor ~Demo( ){instanzenzaehler--;}; //Destruktor }; int Demo::instanzenzaehler=0; //Belegung (Definition!) void main ( ) { Demo lok; //Instanzenbildung von Demo cout<<Demo::instanzenzaehler<<endl; cout<<lok.instanzentaehler<<endl; } Die Ausgabe wird zweimal 1 lauten (untereinander). Datenelemente die mit const (in Java sind dies final Datenelemente, die dort nicht lokal sein dürfen, also Klassenvariablen sein müssen) deklariert werden, können nach der Instanzenbildung nicht mehr verändert werden. Sie müssen in der Konstruktorliste initialisiert werden. class Demo { public: const int j; Demo ( ) :j(2) {…} }; Methoden Altbekannter Weise sind die Funktionen, die als Elemente einer Klasse definiert werden, die Methoden. Es ist möglich, die Methoden innerhalb und außerhalb der Klassendeklaration zu definieren. - Methoden, die innerhalb von Klassen definiert werden, sollten kurz gehalten werden. Zum einen, um die Übersichtlichkeit der Klassendefinition zu wahren, zum anderen um den Speicherplatz zu beschränken. - Definitionen außerhalb der Klassendeklaration muss der Klassenname mit Bereichsoperator (::) vorangestellt werden. Dies wäre in Java undenkbar, die Methoden außerhalb zu definieren. Man könnte sie allenfalls als abstrakte Methoden in einer Sub- Klasse überschreiben, aber das ist nicht im entferntesten zu vergleichen. Allen Methoden sind gewisse Eigenschaften zueigen: - mögliche Deklaration als public, protected oder private - Zugriff auf alle Elemente der Klasse, aus der sie stammen - neben Friends (später dazu mehr bei Zugriff von außerhalb einer Klasse) einzige Möglichkeit zu Zugriff auf private Elemente - Sie können als virtual, volatile oder const deklariert werden. - Sie können nur von Methoden derselben Klasse überladen werden. class Demo { int wert; public: Demo ( ) {wert =1;} Int func( ); }; int Demo::func( ) {…} //Definition in der Klasse (der //Konstruktor ist ja auch eine Methode) //wird „draußen“ definiert //Definition von func Sonderformen von Methoden stellen die statischen und die konstanten Methoden dar. Mit static deklarierte Methoden sind statische Methoden. Sie sind ebenso wie statische Datenelemente nicht instanzen- sondern klassenspezifisch. Sie besitzen keinen this- Zeiger und können nur andere statische Elemente aufrufen. Der Aufruf der statischen Methoden kann über Instanzen oder den Klassennamen erfolgen. Ferner dürfen statische Methoden nicht als virtual deklariert werden. Im Gegensatz zu statischen Datenelementen müssen statische Methoden nicht im Namespace definiert werden. class Demo { private: //eigentlich überflüssig static int standardwert; public: static void std_setzen (int i) { if(i > 0 && i < 100) standardwert = i; } }; int Demo::standardwert = 1; void main ( ) { Demo obj1, obj2; Demo::std_setzen(33); //standardwert ist jetzt bei obj1 //und obj2 =33 } Genauso wie in Java sind statische Methoden Klassenspezifisch! Konstante Methoden werden mit const deklariert, wohlgemerkt hinter der Parameterliste. Außer explizit angeführte mutable Daten, können const Methoden keine Daten verändern und auch nur const Zeiger und Referenzen zurückgeben. Achtung: final Methoden in Java haben, anders als final Datenelemente, nichts mit const Methoden aus C++ zu tun (in Java gibt es wohl kein Äquivalent dafür)! class Demo { public: int i; const int j; mutable int n; Demo( ) : j2 {} void incr ( ) const { i++ ; //Fehler j++ ; //Fehler n++ ; //ok } void func ( ) {…} }; konstante Klasseninstanzen (ist schon ein kleiner Vorgriff): const demo obj2; obj2.func( ); //Fehler, siehe Instanzenbildung obj2.inkr( ); //ok this- Zeiger In allen nichtstattischen Methoden der Klassen ist der this- Zeiger verfügbar. Er wird standardmäßig vom Compiler zu Verfügung gestellt und verweist auf den Wert des Objekts, aus welchem er aufgerufen wurde. Er funktioniert im allgemeinen wie der this- „Zeiger“ von Java. Instanzenbildung Instanzenbildung ist die Einrichtung von Objekten einer Klasse, indem den Datenelementen Werte zugeordnet werden. Für die Einrichtung des Speichers und die Initialisierung der Datenelemente ist der Konstruktor zuständig, zu dem im nächsten Kapitel (endlich) ausführlicheres gesagt wird. Man kann neben der herkömmlichen Instanz (Klassenname Instanzname ([Parameter]) ;) auch const Instanzen oder Arrays von Instanzen bilden. const- Instanzen: - werden bei der Einrichtung als const deklariert. - Alle Datenelemente, die nicht explizit als mutable deklariert sind, sind dann const! - Es können nur als const deklarierte Methoden aufgerufen werden. Array- Instanzen: - Jedes Array- Element muss einen Konstruktor aufrufen. Entweder und meist praktischer den Standard- Konstruktor, oder explizit für jedes Element einzeln (kann sehr umständlich werden) Konstruktor/ Destruktor Konstruktor: Syntax: Klassenname ( ) { } Jede Instanzenbildung von einer Klasse ist mit dem Aufruf des Konstruktors verbunden (wie in Java). Dadurch ergibt sich für die Anwendung: - Möglichkeit für den Programmierer zur Initialisierung von Datenelementen und Eingangsarbeiten (Reservierung dynamischen Speichers) - gesteuerte Instanzenbildung durch Überladung des Konstruktors mit unterschiedlichen Argumententypen Damit es zu keinen Pannen kommt, wenn der Programmierer den Konstruktor vergisst, stellt der Compiler jeder Klasse einen Standardkonstruktor zur Verfügung. Er geht allerdings verloren, wenn der Programmierer eigene Konstruktoren definiert. Grundsätzlich gilt: - der Konstruktorname ist gleich dem Namen der Klasse - ein Konstruktor hat keinen Rückgabewert (auch nicht void) - Konstruktoren können nicht als virtual, static, const, mutable oder volatile deklariert werden - Konstruktoren werden nicht vererbt! - Es können keine Zeiger auf Konstruktoren definiert werden. - Nach der Instanzenbildung sind sie nicht mehr aufrufbar (in der jeweiligen Instanz). - Konstruktoren, die mit genau einem Argument aufgerufen werden können, können implizite Konvertierungen von dem Argumententyp in den Typ ihrer Klasse durchführen. Um ungewollte Konvertierungen zu vermeiden, kann ein solcher Konstruktor als explicit deklariert werden. Beispiel: class demo { int privat; //privat ist private public: demo (int i) {privat=i;} explicit demo (float f) {privat = (int) f;} }; void main ( ) { demo Instanz1 (100); demo Instanz2 = 100; //implizite Konvertierung demo Instanz3 (3.2); demo Instanz4 = 3.2; //Fehler: explicit demo Instanz5; //Fehler: //Standardkonstruktor nicht //mehr verfügbar } Ein besonderer Konstruktor ist der Kopierkonstruktor. Er erzeugt Instanzen seiner Klasse auf Grundlage anderer Instanzen der Klasse. Die Instanz, von der kopiert werden soll, wird dem Konstruktor als Parameter übergeben. Er stellt eine Möglichkeit dar, Klasseninstanzen zu kopieren. Er soll aber an dieser Stelle nicht weiter interessieren. Destruktor: Syntax: ~Klassenname ( ) {} Destruktoren werden automatisch aufgerufen, wenn Klasseninstanzen ihre Gültigkeit verlieren (z.B. beim Verlassen von Gültigkeitsbereichen oder beim Aufruf von delete für Zeiger auf Klasseninstanzen). Sie lösen die Instanz auf und geben deren Speicher frei. Auch für Destruktoren gilt: - Sie tragen den gleichen Namen wie ihre Klasse, jedoch mit einer vorangestellten Tilde. - Sie haben keinen Rückgabewert. - Destruktoren können nicht als static, const, mutable oder volatile deklariert werden. - Sie werden nicht vererbt und es können keine Zeiger auf sie deklariert werden. Doch sie unterscheiden sich auch von den Konstruktoren: - An sie können keine Argumente übergeben werden. - Sie können zwar überschrieben, aber nicht überladen werden. - Destruktoren können als virtual deklariert werden. - Basisklassen sollten im Zweifelsfalle einen virtuellen Destruktor definieren. Destruktoren von Ableitungen dieser Basisklassen sind dann automatisch virtuell. - Destruktoren können explicit aufgerufen werden, ist aber selten notwendig. Allgemein stellt der Compiler für Klassen automatisch 5 Standardversionen von Methoden zur Implementierung bereit: - Standardkonstruktor - Destruktor - Standardkopierkonstruktor (1:1 kopieren) - Zuweisungsoperator - Adressoperator (this- Zeiger) Zugriffe Innerhalb der Klasse Der Zugriff auf Elemente derselben Klasse unterliegt praktisch keinen Beschränkungen. Die Elemente werden einfach mit ihrem Namen angesprochen. Zu beachten sind lediglich: - Es gelten die „normalen“ Gültigkeitsbereiche. - Methoden die als const deklariert sind können zwar auf alle Elemente zugreifen, aber nur mutable- Datenelemente verändern. - Statische Methoden können nur statische Elementdaten direkt ansprechen. Nichtstatische Elemente müssen über eine existierende Instanz angesprochen werden. Außerhalb der Klasse Von außerhalb kann auf Elemente einer Klasse X nur über eine Instanz der Klasse X in Verbindung mit einem der Operatoren . -> .* ->* oder über Friends der Klasse zugegriffen werden. Zugriff über Instanzen: - mit . oder -> - Elemente , die als protected oder private deklariert wurden, können so nicht angesprochen werden. Auch mit Zeigern (.* oder ->*) können nur public Elemente angesprochen werden. Zugriff über Friends: Um die strengen Regeln der Zugriffsberechtigung in Ausnahmefällen (wegen einer Ausnahme stellt man ja nicht unbedingt den Status einer private- Variable in Frage, das könnte das ganze Programm durcheinanderbringen oder gefährden) zu umgehen kann man sich der Friends bedienen. Voraussetzungen um eine Friend- Funktion einzurichten: - es muss die Funktion der Klasse, auf die sie Zugriff haben soll , als Friend deklariert werden, - es muss eine Referenz oder Instanz der Klasse an die Funktion übergeben werden. Zu beachten: - Die Deklaration als Friend muss in der Klasse geschehen! - Neben der Deklaration einzelner Friend- Funktionen kann man auf einen Schlag alle Funktionen als Friend deklarieren, indem man die Klasse als Friend deklariert. Aus der abgeleiteten Klasse auf Basisklassenelemente Die Methoden der abgeleiteten Klasse können sowohl auf die eigenen wie auch auf die ererbten Elemente zugreifen, indem sie sie einfach mit dem Namen ansprechen. Während es für die eigenen Datenelemente keine weiteren Beschränkungen gibt, können ererbte Element nur dierekt angesprochen werden, wenn sie nicht als private deklariert wurden. Sonst muss man den Umweg über geerbte public oder protected- Methoden gehen. Von außen Es macht von außen keinen Unterschied aus, ob das Element aus der Basisklasse kommt oder aus der abgeleiteten Klasse, wichtig ist nur, welche Zugriffsrechte die abgeleitete Klasse für die geerbten Elemente nach außen weitergibt (Wer das hier und jetzt nicht versteht, sollte sich das ganze noch einmal nach dem Kapitel Vererbung durchlesen!). Der Rest (laut Übersicht) im Vortrag von Anke enthalten!