Klassen und ähnliche Elemente und Strukturen in C++, im Vergleich

Werbung
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!
Herunterladen