definiert wurde, entspricht die Klasse
Das Schlüsselwort struct wurde hier durch class ersetzt und public leitet die Definition der
Komponenten ein. Die Kennzeichnung public (= öffentlich) sagt, dass s1, s2
und so weiter public = öffentlich sind.
Ein Verbund ist eine Klasse deren Komponenten zunächst einmal öffentlich (public) sind. Umgekehrt sind Klassen Verbunde, bei denen alle Komponeten zunächst einmal privat (private) sind. Einer Klasse K, die als
definiert wurde, entspricht der Verbund
In Klassen ist also alles privat und in einem Verbund ist alles public. Durch Angabe der Schlüsselworte public
und private kann diese Voreinstellung verändert werden.
Der Einfachheit halber, und entsprechend der üblichen Praxis, belassen wir es im Folgenden dabei, dass in Verbunden alles öffentlich ist und benutzen Klassen, wenn zwischen öffentlich und privat unterschieden werden soll.
ist das Gleiche wie der Verbund
Lässt man in der Klassendefinition aber public weg, dann werden x und y zu privaten Komponenten
von Vektor:
class Vektor { float x, y; }; // x und y sind privat
Mit dem Schlüsselwort private kann man dies auch explizit ausdrücken:
class Vektor {
private:
float x, y; // x und y sind privat
};
Öffentliche und private Komponenten können in einer Klasse auch gemischt auftreten:
class Vektor {
public:
float x;
private:
float y;
};
x ist jetzt öffentlich und y ist privat. Genau wie Datenkomponenten können auch Methoden
- inklusive Operatoren - öffentlich oder privat sein. Beispiel:
class Vektor {
public:
Vektor operator+ (Vektor); // oeffentliche Methode
private:
float x, y; // private Daten
};
Nur die Komponenten einer Klasse, die in der Definition der Klasse hinter dem
Schlüsselwort public erscheinen, sind öffentlich, die anderen sind privat.
Beispiel:
class S1 {
int x; // privat
int f (int); // privat
public:
int y; // oeffentlich
int g (int); // oeffentlich
};
Mit dem Schlüsselwort public wird der private Teil beendet und ein
öffentlicher begonnen. Mit private wird ein öffentlicher Teil beendet und ein
privater begonnen. Die beiden Schlüsselworte können beliebig oft und
in beliebiger Reihenfolge angewendet werden. Beispiel:
class S2 {
int x; // privat
public:
int y; // oeffentlich
int g (int); // oeffentlich
private:
int f (int); // privat
};
class S3 {
int x; // privat
public:
int f(int); // oeffentlich
int y; // oeffentlich
};
Die Komponente x kann nur von der Klasse S3 selbst verwendet werden. Beispielsweise in der
Methode f, die zu S3 gehört:
int S3::f (int p) {// Wir sind in S3::f, also innerhalb von S3
++x; // OK: privat aber intern genutzt
return y; // OK: y ist oeffentlich
}
Außerhalb des Sichtbarkeitsbereichs von S3 ist x im Gegensatz zu y und f nicht
zugreifbar:
int main () { // Wir sind in main, also ausserhalb von S3
S3 s;
s.x = 10; // FEHLER: x ist eine private Komponente der Klasse S3
++s.y; // OK : y ist "offentlich
}
Hier wird von main aus (der Punktoperator ``.x'' taucht in main auf)
versucht auf s.x zuzugreifen, das ist nicht erlaubt.
x ist eine private Komponente von S3 und der ``Zugreifer'' main gehört natürlich nicht zu S3.
Der Zugriff auf s.y ist dagegen von überall her uneingeschränkt möglich.
Der Zugriff auf private Komponenten einer Klasse ist darum nur von dieser Klasse aus erlaubt. Es kommt dabei darauf an,
wo der Zugriff (der Punktoperator) auftaucht; in einer Methode der Klasse: OK!, Irgendwo anders: Nicht OK!
class C {
public:
int a;
void g();
private:
int b;
};
C x, y;
void C::g() {// wir sind in C: alles erlaubt
++b; // OK: eigene private Komponente
++x.b; // OK: private Komponente von x
}
int main () { // wir sind in main, nicht in C: nur Zugriff auf public erlaubt
y.g(); // OK: y greift in Methode g auf Privates von sich selbst und von x zu
++x.b; // FEHLER
}
Innerhalb der Klasse C, also in einer Methode von C, ist jeder Zugriff erlaubt, außerhalb
nur der auf öffentliche Komponenten. Von welchem Objekt dabei auf welches andere Objekt zugegriffen
wird, ist völlig irrelevant.
Klassen definieren die Familie ihrer Objekte. Innerhalb einer Familie gibt es keine Privatsphäre. Mitglieder der eigenen Familie dürfen alles anfassen. Fremden klopft der Compiler allerdings auf die Finger, wenn sie die Privatsphäre der Familie (= Klasse) nicht achten.
private ist also gar nicht so privat. Innerhalb einer Methode einer Klasse S kann auf die privaten Komponenten aller dort sichtbaren Objekte der Klasse S zugegriffen werden. Sichtbar sind nach den üblichen Sichtbarkeitsregeln die lokalen Variablen, die Parameter und die globalen Variablen - soweit jeweils vorhanden.
Mit private und public können die Komponenten einer Klasse in zwei Gruppen aufgeteilt werden. Private Komponenten sind nur für interne Zwecke da. Sie sind Hilfskonstrukte der Implementierung und gehen die Benutzer der Klasse nichts an. Die öffentlichen sind dagegen dazu gedacht, von außen benutzt zu werden. Mit der Unterscheidung in privat und öffentlich können die Komponenten einer Klasse sortiert werden. Man nennt dies das Prinzip der Kapselung:
Diese Unterscheidung ist für die Wirklichkeit der Softwarebranche zu grob. Ein Entwickler hat es nur recht selten mit dem sogenannten End-Benutzer zu tun. Da Software normalerweise im Team entwickelt wird, kommuniziert er meist mit anderen Entwicklern. Zwischen den Entwicklern, die an einem Projekt arbeiten, bestehen ebenfalls Benutzt-Relationen. Softwarekomponenten benutzen andere Komponenten um ihre eigene Funktion zu erfüllen. In folgenden Beispiel benutzt die kgv-Funktion die ggt-Funktion:
unsigned int ggt(unsigned int a, unsigned int b) {
while (a != b)
if ( a > b ) a = a-b; else b = b-a;
return a;
}
unsigned int kgv(unsigned int a, unsigned int b) {
return a*b/ggt(a,b);
}
Die Benutzt-Relation zwischen Softwarekomponenten ist von ganz entscheidender Bedeutung für die Strukturierung
von Software. Ein Benutzer ist darum im Folgenden in der Regel ein Stück Software bzw. dessen Autor.
Der Mensch, der ein fertiges Produkt benutzt, wird explizit End-Benutzer genannt.
Da alle halbwegs komplexen Systeme in Komponenten aufgeteilt werden, ist die Trennung von Schnittstelle und Implementierung ein weitverbreitetes Konstruktionsprinzip technischer Systeme, das das Leben enorm erleichtert. Ein Lichtschalter beispielsweise hat eine Schnittstelle und eine Implementierung. Man kann mit ihm das Licht an- und ausmachen, indem die Schalterfläche umgelegt wird. Das ist seine Schnittstelle. Intern werden Kontakte geschlossen. Das ist die Implementierung. Selbstverständlich ist es auch möglich den Schalter aufzuschrauben und die Kontakte direkt zu verbinden. Das ist aber eine ebenso unübliche wie unerwünschte Art den Schalter zu bedienen.
Dieses bewährte Prinzip wird auf die Software übertragen indem man streng unterscheidet zwischen den öffentlichen, für den Benutzer gedachten, Komponenten einer Klasse und den internen, den privaten. (Siehe Abbildung 1):
//Die Klasse:
struct Menge {
// Zugriff (Schnittstelle):
void einfuege (int);
void entnehme (int);
bool istEnthalten (int);
// interne Speicherung:
int m[10]; // Mengenelemente
int a; // Zahl der belegten Pl"atze in m
};
//Ihr Benutzer:
int main () {
Menge m;
m.einfuege (1); // erwuenschter Zugriff
m.a = 2; // nicht erwuenschter Zugriff
m.m[2] = 3; // nicht erwuenschter Zugriff
}
Die Schnittstelle besteht hier aus den Methoden einfuege, entnehme und istEnthalten.
Die Komponenten m und a dienen der internen Speicherung und Verwaltung der Mengenelemente.
Sie sollten nicht direkt benutzt werden. Ihre genaue Verwendung ist für die Benutzung des Typs darum
irrelevant: sie gehören zur Implementierung. Der Benutzer sollte seine Finger von ihnen lassen.
Durch Aufruf der Methode einfuege sollten Elemente eingefügt werden. Man kann statt dessen auch direkt auf m und a zugreifen. Das ist aber so unerwünscht wie das Einschalten des Lichts durch Abschrauben des Schalterdeckels und Kurzschluss der internen Kontakte. Man kann darüber den Kopf schütteln, verhindern kann man es aber nicht.
// Die Klasse
class Menge {
public: // Aha, Schnittstelle:
void einfuege (int); // zur allgemeinen Benutzung
void entnehme (int); // freigegeben!
bool istEnthalten (int);
private: // Aha, Implementierung:
int m[10]; // geht nur Mengenimplementierer
int a; // was an!
};
//Ihr Benutzer:
int main () {
Menge m;
m.einfuege (1); // OK, Zugriff moeglich
m.a = 2; // FEHLER: Zugriff nicht moeglich
m.m[2] = 3; // FEHLER: Zugriff nicht moeglich
}
Die Aufteilung in Schnittstelle und Implementierung in diesem Beispiel ist:
Klassen werden durch Rechtecke dargestellt. Oben enthalten sie den Klassennamen, es folgen die Datenkomponenten und dann die Methoden. Öffentliche Komponenten werden mit einem + gekennzeichnet, private mit einem -. Alles mit einem + gehört also zur Schnittstelle. So wie in diesem Beispiel sind sehr oft alle Datenkomponenten privat. Die Kennzeichnungen + und - kïnnen auch weggelassen werden.
Die Aufteilung eines Stücks Software in Schnittstelle und Implementierung kann in UML leider nur mit optionalem + und -, ausgedrückt werden. 2
Die klare Trennung von Interna und benutzbarer Schnittstelle nutzt zunächst einmal den Benutzern/Anwendern der Klasse. Sie können sich auf das konzentrieren, was als öffentlich deklariert und somit zur Benutzung freigegeben wurde.
Jeder, der ein komplexes System anwenden muss, wird dankbar alles zur Kenntnis nehmen, was er nicht kennen oder wissen muss. Ein Lichtschalter wird einfach gedrückt. Ich muss mir dabei keine Gedanken darüber machen, ob es sich um einen einfachen Schalter oder einen Wechselschalter handelt, ob er ein Relais bedient oder direkt schaltet, welche Kontakte verbunden werden, und so weiter. Ich sehe die Schnittstelle und kann das Ding bedienen.
Die Elektrik eines Hauses kann geändert werden, ohne dass jeder, der das Licht anschalten will, einen Lehrgang besuchen muss. Alle Lichtschalter haben die gleiche Schnittstelle, egal, ob es sich um Wechsel- oder Relaisschalter handelt. Die einen können darum jederzeit ohne Absprache gegen die anderen ausgetauscht werden.3 Genauso kann die Implementierung der Menge geändert werden, ohne die Benutzer auch nur darüber informieren zu müssen.
Die Kopplung von Softwarekomponenten sollte einerseits klar erkennbar und andererseits so klein wie möglich sein. Der Grad der Kopplung wird durch das Geschick oder Ungeschick der Software-Entwickler bestimmt (siehe Abbildung 4). Die Unterscheidung von Schnittstelle und Implementierung kann die Kopplung nur dokumentieren: alles was zur Schnittstelle gehört, aber nur das, kann in einer anderen Komponente benutzt werden. Die Kopplung zwischen dem Bneutzer und der Realisation einer Klasse wird auf die Schnittstelle - also die öffentlichen Datenfelder und Methoden - begrenzt.
Eine Trennung von Privatem und Öffentlichem gibt es auch bei Verbunden - man denke nur an struct Menge {...};. Bei einer Klasse kann sie aber im Code mit den Schlüsselworten private und public dargelegt und so vom Compiler überwacht werden.
Es bleibt natürlich weiterhin den Entwicklern überlassen zu entscheiden, was zu welcher Kategorie gehört. Diese Entscheidung ist oft genug weder eindeutig noch einfach.
Dies alles bezieht sich nur auf den Quelltext von Programmen. Was wann welcher Mensch sehen oder nicht sehen darf, ist eine völlig andere Frage. Ob die Programmiererin, die die Definition der Klasse Menge im Beispiel oben benutzt, die Definition der privaten Komponenten m und a mit eigenen Augen sehen darf, hat vielleicht etwas mit dem Betriebsklima oder mit Lizenzverträgen zu tun. Es wird ihr aber vom Compiler weder verboten noch erlaubt. Er kann entsprechende Regeln nicht einmal überwachen.
Der Compiler interessiert sich nicht für die Beziehungen zwischen Menschen sondern nur für Programmtexte. Er prüft und übersetzt einen Text ohne zu wissen von wem er stammt. Mit private kann der Programmierer dem Compiler nur eine Absicht für die Organisation des Quelltextes bekannt gegeben werden. Dieser prüft dann, ob diese Absicht im gesamten Programm auch eingehalten wird.
private hat also ganz und gar nichts mit der Privatsphäre, den Geheimnissen oder den Besitzverhältnissen zwischen Menschen zu tun. Es trennt lediglich die Schnittstelle (das ``"Offentliche'') und die Implementierung (das ``Private'') in einem Stück Software.
Ein Freund (in C++) ist jemand, der die Privatsphäre nicht achten muss. Klassen können Funktionen und andere Klassen zu ihren Freunden erklären. Der Freund hat dann vollen Zugriff auf alle privaten Komponenten.
Freundschaft beruht (zumindest in C++) nicht unbedingt auf Gegenseitigkeit. Wenn Klasse A eine Klasse B zu ihrem Freund erklärt, erlaubt sie B den Zugriff auf ihre Privatsphäre. Damit hat sie selbst aber noch lange keinen Zugriff auf die privaten Komponenten von B.
class Vektor {
friend void drucke (Vektor); // drucke ist mein Freund
public: // und darf auf x und y zugreifen
...
private: // drucke darf hier zugreifen
float x, y;
};
drucke darf damit auf die privaten Komponenten x und y jedes Objekts vom
Typ Vektor zugreifen:
void drucke (Vektor v) {
cout << "(" << v.x << ", " << v.y << ")\n";
}
Befreundete freie Funktionen sind damit eine Alternative zu Methoden. Genau wie Methoden dürfen sie auf die
privaten Komponenten einer Klasse zugreifen.
Auch freie Operatoren können Freunde einer Klasse sein:
class Vektor {
friend void drucke (Vektor);
friend Vektor operator+ (Vektor, Vektor);
public:
...
private:
float x, y;
};
// befreundeter freier Operator:
Vektor operator+ (Vektor v1, Vektor v2) {
Vektor res;
res.x=v1.x+v2.x; res.y=v1.y+v2.y;
return res;
}
// Operator als Methode:
Vektor Vektor::operator- (Vektor v2) {
Vektor res;
res.x=x+v.x; res.y=y+v.y;
return res;
}
Mit dem Konzept der Freundschaft sollte sparsam und mit Überlegung umgegangen werden.
Es setzt private ausser Kraft und schaltet
damit etwas ab, das allgemein als wichtiges und sinnvollen Prinzip der Software-Entwicklung angesehen wird.
class Vektor {
friend class Punkt; // Klasse Punkt ist mein Freund
public:
...
private:
float x, y;
};
class Punkt {
public:
...
float entfernt_von (Punkt);
private:
Vektor pos;
};
...
// Alle Methoden von Punkt duerfen auf alle
// Komponenten von Vektor zugreifen.
...
float Punkt::entfernt_von (Punkt p) { // Zugriff auf die privaten Komponenten von pos
return sqrt ( (pos.x - p.pos.x)*(pos.x - p.pos.x)
+
(pos.y - p.pos.y)*(pos.y - p.pos.y));
}
Hier darf jede Methode von Punkt auf alles von Vektor zugreifen, aber nicht
umgekehrt.4
Auch mit der Freundschaft zwischen Klassen sollte sparsam umgegangen werden. Nur Klassen die inhaltlich sehr eng zusammengehören sollten Freunde sein.
struct Vektor {
...
float x;
float y;
};
...
Vektor operator+ (Vektor a, Vektor b) {
Vektor res;
res.x = a.x + b.x;
res.y = a.y + b.y;
return res;
}
...
Vektor a, b, c;
...
a = b + c;
Die Definition eines solchen Operators unterscheidet sich kaum von der einer ``normalen'' freien Funktion.
Die Anweisung
a = b + c;
ist als
a = operator+ (b, c);
Ein binärer Operator, z.B. der operator+, wird als Methode stets mit einem Argument definiert. Ist operator+ eine Methode (mit einem Argument) von a dann wird
a+b
als
a.operator+(b)
interpretiert. Unäre Operatoren werden entsprechend als Methoden ohne Argument definiert. Ist beispielsweise operator++ eine Methode (ohne Argument) von a dann wird
++a
als
a.operator++ ()
interpretiert.
Die Vektoraddition mit ``+'' als Methode ist:
class Vektor {
public:
...
Vektor operator+ (Vektor b); // binaeres +
...
private:
float x;
float y;
};
...
Vektor Vektor::operator+ (Vektor b) { // addiere b zu mir!
Vektor res;
res.x = x + b.x;
res.y = y + b.y;
return res;
}
...
Vektor x, y, z;
x = y + z;
Operatoren als Methoden können, im Gegensatz zu Operatoren als freie Funktionen, auf private Komponenten
zugreifen.
class Vektor {
public:
...
Vektor operator- () { // unaeres Minus
Vektor res; // als Methode
res.x = -x; res.y = -y;
return res;
}
...
private:
float x;
float y;
};
// Benutzung:
int main () {
Vektor a, b;
b = -a; // unaeres Minus auf einem Vektor
}
Sie können auch als freie Funktionen definiert werden:
Vektor operator- (Vektor v) { // unaeres - als freie Funktion
return Vektor (-v.x, -v.y);
}
Die freie Funktion wird genauso wie die Methode benutzt.
int max (int a, int b) {
if (a > b) return a; else return b;
}
kann die Lesbarkeit eines Programms beträchtlich erhöhen. Man darf aber den erhöhten Aufwand der Funktion
nicht vergessen. Wird eine solche kleine Funktion sehr oft aufgerufen - speziell in einer Schleife -,
dann wird die verbesserte Lesbarkeit mit einem schlechteren Laufzeitverhalten erkauft.
Mit inline Funktionen kann die Lesbarkeit verbessert werden, ohne dass die Effizienz des Programms leidet.
inline int max (int a, int b) {
if (a > b) return a; else return b;
}
Das Schlüsselwort inline weist den Compiler an, nicht wirklich eine Funktion zu erzeugen und aufzurufen, sondern
jeden Funktionsaufruf im Quellprogramm durch den Code des Funktionskörpers zu ersetzen.
Nur wirklich kleine und einfache Funktionen sollten inline sein. Zum einen erzeugt ein Funktionsaufruf nur einen geringen Zusatzaufwand und zum anderen kann mit der Expansion umfangreicher Funktionen der Maschinencode wesentlich größer werden, was eventuell auch negative Auswirkungen auf das Laufzeitverhalten hat. Komplexe Funktionen können u.U. auch vom Compiler gar nicht korrekt expandiert werden. Die Inline-Direktive ist darum nur ein Hinweis, den der Compiler befolgen kann, aber nicht befolgen muss.
class C {
public:
inline int f ();
int x;
};
inline int C::f () { return x; }
Das Schlüsselwort inline ist hier sowohl der Deklaration der Methode als auch der Definition vorgestellt.
Eines der beiden kann weggelassen werden.
class C {
public:
inline int f () { return x; }
int x;
};
Da dies nur für inline Methoden möglich ist, ist das Schlüsselwort inline redundant und kann weggelassen
werden:
class C {
public:
int f () { return x; } // Inline Methode
int x;
};
Es ist üblich kleine Methoden in dieser Art inline zu definieren.
class Menge {
public:
void init () { a = 0; }
void einfuege (int);
void entnehme (int);
bool istEnthalten (int);
private:
int m[10];
int a;
};
Jede Variable vom Typ Menge muss dann vor der ersten Benutzung mit init initialisiert werden.
... Menge m1, m2; m1.init(); m2.init(); ...
class Menge {
public:
Menge (); // Konstruktor (Deklaration)
...
};
Menge::Menge () { a = 0; } // Konstruktor (Definition)
bzw.
class Menge {
public:
Menge () { a = 0; } // Konstruktor (inline)
...
};
Durch diese einfache Konvention der Benennung kann also ein Konstruktor
vom Compiler erkannt werden. Er nutzt sein Wissen, um an allen Stellen, an denen
ein neues Exemplar des Typs Menge erzeugt wird, selbständig einen Aufruf des Konstruktors
einzufügen:
... Menge m1, m2; // Hier fuegt der Compiler (in dem von ihm erzeugten Code) // automatisch die Aufrufe // m1.Menge() und // m2.Menge() ein ...Explizite Initialisierungen sind nicht mehr notwendig. Sie können somit auch nicht mehr versehentlich vergessen werden.
class C {
public:
int a;
};
C c1;
int main () {
C c2;
}
c1.a wird hier mit 0 initialisiert, c2.a enthält einen Zufallswert.
Aufrufe von Konstruktoren werden also vom Compiler automatisch in das Quellprogramm eingefügt. Definitionen von Konstruktoren werden dagegen (im Allgemeinen) nicht automatisch erzeugt.
class Menge {
public:
Menge (); // Konstruktor 1
Menge (int); // Konstruktor 2
Menge (int, int); // Konstruktor 3
...
};
Menge::Menge () { a = 0; }
Menge::Menge (int x) { a = 1; m[0] = x; }
Menge::Menge (int x, int y) { a = 2; m[0] = x; m[1] = y; }
...
Menge m1; //Variablendef. plus Aufruf von Konstruktor 1
Menge m2 (1); //Variablendef. plus Aufruf von Konstruktor 2
Menge m3 (1, 2); //Variablendef. plus Aufruf von Konstruktor 3
Konstruktoren mit Argumenten werden aktiviert, indem man die Argumente in Klammern hinter
die definierte Variable schreibt:
Menge m2(1);
oder den Konstruktor explizit aufruft:
Menge m2 = Menge::Menge(1);
Menge m2 = Menge(1); // explizite Konversion mit Typname als Konstruktor
Eine implizite Konversion ist auch möglich:
Menge m2 = 1; // OK: implizite Konversion mit Konstruktor
Hier wird der int-Wert 1 implizit in ein Objekt vom Typ Menge konvertiert. Der Konstruktor Menge::Menge(int) dient dabei als implizite Konversionsfunktion.
class Menge {
public:
...
explicit Menge (int);
...
};
...
Menge m1 (1); // OK: expliziter Aufruf des Konstruktors
Menge m2 = 1; // FEHLER: nicht erlaubte Verwendung des Konstruktors zur Konversion
Mit explicit sollen Fehler durch unbeabsichtigte Konversionen vermieden werden.
Man beachte dass der Default-Konstruktor ohne die Verwendung von Klammern aktiviert wird:
Menge m; // OK statt
Menge m(); // FALSCH
Die Konstruktion Menge m() würde vom Compiler nicht als Definition einer Variablen m, sondern als Deklaration einer Funktion m mit Ergebnis vom Typ Menge missverstanden werden.
class Menge {
public:
// KEIN DEFAULT-Konstruktor
Menge (int);
Menge (int, int);
...
};
...
Menge m1; // FEHLER: Compiler sucht vergeblich Menge::Menge ()
Menge m2(1); // OK
Enthält eine Klasse überhaupt keinen Konstruktor, dann muss auch kein Default-Konstruktor
vorhanden sein:
class Menge {
public:
// KEIN Konstruktor
...
};
...
Menge m1; // OK: gar kein Konstruktoraufruf:
// m1 hat Zufallswert, oder -- falls es global ist --
// ist mit 0-en belegt,
Menge m2(1); // FEHLER: Compiler sucht vergeblich Menge::Menge (int)
Ist kein Konstruktor definiert, dann wird auch kein Default-Konstruktor erwartet.
Gibt es irgendeinen Konstruktor, dann muss bei Bedarf auch ein Defaultkonstruktor zur Verfügung stehen.
Man sollte von der Möglichkeit alle Konstruktoren wegzulassen jedoch nur überlegt Gebrauch machen. Jede Klasse sollte mit mindestens einem Konstruktor ausgestattet werden um fehlerhafte oder zufällige Initialisierungen schon bei der Übersetzung aufspüren zu können.
Im Regelfall sollte auch ein Defaultkonstruktor definiert werden. Nur wenn keine sinnvollen allgemeinen Initialwerte für die Datenkomponenten angegeben werden können, kann er fehlen. In dem Fall sollte es dann aber mindestens einen anderen Konstruktor geben.
class Menge {
public:
// KEIN Konstruktor
// und ALLE Datenkomponenten public
int m[10];
int a;
};
...
Menge m = {{1,2,3,4,5,6,7,8,9,0}, 10}
Initialisierungslisten eignen sich zur Belegung großer Strukturen.
Generell sollten Objekte aber nicht mit Initialisierungslisten sondern mit Konstruktoren
initialisiert werden.
class Menge {
public:
Menge (); // Konstruktor
~Menge (); // Destruktor
...
};
.. f (...) {
Menge m;
// Beginn der Existenz von m:
// Compiler-erzeugter Aufruf des Konstruktors
// m.Menge();
...
...
// Ende der Funktion und damit
// Ende der Existenz aller funktionslokalen Variablen:
// Compiler-erzeugter Aufruf des Destruktors
// m.~Menge();
}
Destruktoren werden für Aufräumarbeiten eingesetzt, die beim Ableben eines
Objektes notwendig werden. Typischerweise werden im Destruktor die vom dahingehenden
Objekt belegten Ressourcen freigegeben. Dies kann bei der Verwendung von Verweisen
sinnvoll sein. Im Gegensatz zu Konstruktoren sind Destruktoren oftmals nicht
notwendig. Sie werden wie Konstruktoren vom Compiler nicht automatisch erzeugt.
Destruktoren werden später ausführlicher behandelt.
int i = 5;
int a[3] = { 1, 2, 3 };
Klassen - inklusive ihr Sonderfall Struct - werden durch Konstruktoren initialisiert. Nur dann, wenn kein Konstruktor existiert und alle Komponenten öffentlich sind, ist die Zuweisungsinitialisierung auch bei ihnen erlaubt.
struct S { int x, y, z; };
S s = { 1, 2 , 3 };
Ohne explizite Initialisierung werden statische Variablen mit Null belegt und alle anderen behalten ihre Zufallswerte.
class Vektor {
public:
Vektor ();
Vektor (float, float);
private:
float x, y;
};
Vektor::Vektor () { x = 0.0; y = 0.0; }
Vektor::Vektor (float a, float b) { x=a; y=b; }
Wird ein Objekt vom Typ Vektor angelegt, dann werden zuerst dessen x- und y-Komponenten
initialisiert, und zwar nach den Regeln, die für float-Variablen gelten (also keinerlei Initialisierung
außer für statische Objekte). Danach wird der Konstruktor aufgerufen.
Etwas interessanter wird die Angelegenheit, wenn ein Objekt nicht aus skalaren Typen wie float zusammengesetzt ist, sondern aus ``richtigen Objekten'' besteht. Nehmen wir eine Gerade, die mit Hilfe von zwei Vektoren dargestellt wird:
class Gerade {
public:
Gerade ();
Gerade (Vektor, Vektor);
private:
Vektor o, r;
};
Gerade::Gerade () { o = Vektor (0.0, 0.0); r = Vektor (1.0, 1.0); }
Gerade::Gerade (Vektor po, Vektor pr) { o = po; r = pr; }
Wird ein Objekt vom Typ Gerade angelegt, z.B. mit
Gerade g;
dann laufen folgende Initialisierungsaktionen ab:
Das Prinizip, das die Initialisierungsreihenfolge bestimmt, ist insgesamt sehr einfach:
g.r.Vektor()
mit g.r.x=0.0, g.r.y=0.0 belegt, dann wird g.Gerade() aufgerufen, die Zuweisung
r = Vektor (1.0, 1.0);
in g.Gerade() ausgeführt und g.r erhält seinen endgültigen Wert g.r.x=1.0, g.r.y=1.0.
Die letzte Zuweisung könnten wir uns sparen, wenn der Konstruktor für g.r gleich mit den richtigen Argumenten aufgerufen würde. Statt durch den Defaultkonstruktor müßte g.r also gleich mit g.r.Vektor(1.0, 1.0) belegt werden. Zu dem notwendigen gezielten Aufruf eines Konstruktors für Unterobjekte gibt es Initialisierer.
class Gerade {
public:
Gerade ();
Gerade (Vektor, Vektor);
private:
Vektor o, r;
};
Gerade::Gerade ()// Defaultkonstruktor mit Initialisierer
: o(0.0, 0.0), // Initialisierer: expliziter Aufruf des Konstruktors
// Vektor::Vektor(float, float) fuer Komponente o
// statt des Defaultkonstruktors Vektor::Vektor()
r(1.0, 1.0) // Initialisierer: expliziter Konstruktoraufruf
// fuer Komponente r
{} // Keine Zuweisung da bereits korrekt initialisiert
Gerade::Gerade (Vektor po, Vektor pr)
: o(po), r(pr)
{}
Im Konstruktor Gerade::Gerade werden o und r jezt gleich durch den richtigen
Konstruktoraufruf mit den endgültigen Werten belegt, statt sie erst durch ihren
Default-Konstruktor zu initialisieren und dann durch eine Zuweisung mit den richtigen Werten zu belegen.
In dieser Variante ist die Initialisierungsreihenfolge immer noch die gleiche wie oben:
class Vektor {
public:
Vektor ();
Vektor (float, float);
private:
float x, y;
};
Vektor::Vektor () : x(0.0), y(0.0) {}
Vektor::Vektor (float a, float b) : x(a), y(b) {}
class Gerade {
public:
Gerade ();
Gerade (Vektor, Vektor);
private:
Vektor o, r;
};
Gerade::Gerade () : r(1.0, 1.0) {}
Gerade::Gerade (Vektor po, Vektor pr) : o(po), r(pr) {}
Ob man sich bei der Initialisierung gegebenenfalls auf den Defaultkonstruktor verlässt wie in:
Gerade::Gerade (): r(1.0, 1.0) {}
oder lieber alle Konstruktoren explizit aufruft:
Gerade::Gerade (): o(0.0, 0.0), r(1.0, 1.0) {}
das bleibt dem persönlichen Geschmack überlassen. Selbstvertändlich ist auch
Gerade::Gerade (): o(Vektor(0.0, 0.0)), r(Vektor(1.0, 1.0)) {}
möglich. Da Konstruktoren sehr oft aufgerufen werden, kann ihre Effizienz die des Gesamtprogramms stark beeinflussen. Initialisierer können die Laufzeit eines Programms beträchtlich verbessern. In der ersten Version von Gerade wird g.r.x zwei bzw. dreimal mit einem Wert belegt. In der zweiten Variante dagegen nur genau einmal. Werden Tausende von Geraden erzeugt, dann kann das die Laufzeit des des Programm schon beeinflussen.
class Float { // Klasse mit Konstruktor
public:
Float () { f = 0.0; }
float f;
};
class C { // Klasse ohne Konstruktor
public:
Float a; // Klassen-Komponente: wird mit Konstruktor initialisiert
float b; // keine Klasse
};
C c1;
int main () {
C c2;
}
Die Komponenten mit Klassentyp, c1.a und c2.a, werden mit ihrem Default-Konstruktor Float::Float()
initialisiert, obwohl die Klasse C, zu der sie gehören, keinen Konstruktor hat. Der Compiler
erzeugt dazu einen Defaultkonstruktor von C der a.Float() aufruft.
Die Komponenten mit skalarem Typ werden wie üblich behandelt: c1.b wird mit 0 initialisiert, da c1
eine globale Variable ist; c2.b wird nicht initialisiert.
Die Aktivierung der ``Subkonstruktoren'' übernimmt ein vom Compiler generierter Default-Konstruktor:
class Float {
public:
Float () : f(0.0) {}
float f;
};
...
class C {
// C () { a.Float(); } generierter Default-Konstruktor,
public: // Pseudocode, nicht vollwertig
Float a;
float b;
};
...
int main () {
C c; // OK, Aufruf des automatisch erzeugten C::C initialisiert c.a
}
Der erzeugte Defaultkonstruktor ist nicht ``vollwertig'' und kann einen explizit definierten Defaultkonstruktor
nicht ersetzen:
class Float {
public:
Float () : f(0.0) {}
float f;
};
...
class C {
// C () { a.Float(); } generierter Default-Konstruktor,
// Pseudocode, nicht vollwertig
public:
C (float x) : b(x) {} // <- explizit definierter Konstruktor,
Float a; // erzwingt Existenz eines passenden Konstruktors
float b; // bei jeder Erzeugung eines C-Objekts
};
...
int main () {
C c; // FEHLER: Defaultkonstruktor von C fehlt
}
Die Klasse C enthält hier einen explizit definierten Konstruktor, wenn ein C-Objekt erzeugt wird, muss darum der
entsprechende Konstruktor definiert sein. Der vom Compiler erzeugt Default-Konstruktor ist kein
vollwertiger Konstruktor und ``gilt nicht''.
Insgesamt gilt: Der Default-Konstruktor ist der Konstruktor, der ``default-mäßig'' aufgerufen wird. Er muss vorhanden sein, wenn er
Beispiel:
class Vektor {
public:
Vektor ();
Vektor (float, float);
~Vektor ();
static int vz; // <<--- Deklaration statische Datenkomponente
private:
float x, y;
};
int Vektor::vz = 0; // <<--- Definition statische Datenkomponente
Vektor::Vektor () { x=0; y=0; ++vz; } // Konstr. erhoeht vz
Vektor::~Vektor () { --vz; } // Destr. erniedrigt vz
Vektor::Vektor (float a, float b) { x=a; y=b; ++vz; } // Konstr. erhoeht vz
In diesem Beispiel wird eine statische Datenkomponente benutzt, um die Anzahl der Objekte
der Klasse Vektor zu zählen. Die Anzahl wird für alle Objekte der Klasse Vektor
gemeinsam in
int Vektor::vz
geführt. Diese Variable gibt es genau einmal, egal wieviele Vektor-Objekte gerade existieren. Jedesmal, wenn ein Objekt vom Typ Vektor angelegt wird, wird auch vz erhöht. Der Destruktor zählt vz wieder zurück. Wir erinnern uns, dass der Destruktor automatisch aufgerufen wird, wenn ein Objekt verschwindet.
Statische Datenfelder müssen außerhalb der Klassendefinition angelegt (definiert) und bei Bedarf auch dort initialisiert werden:
int Vektor::vz = 0;
Statische Datenkomponenten einer Klasse sind nicht Bestandteil der Objekte, sie sind wie globlale Variablen einmal im Programm vorhanden. Sie existieren also nicht wie ``normale'' Datenkomponenten einmal pro Objekt, sondern einmal pro Klasse. Man nennt sie darum auch Klassenvariablen. Klassenvariablen benutzt man typischerweise um Informationen zu verwalten, die sich nicht auf einzelne Objekte sondern auf die Klasse als Ganzes beziehen.
Zusammengefasst, der Unterschied von ``normalen'' und statischen Datenkomponenten ist:
Vektor v; v.x=0; // x von Vektor v auf 0 setzen
Bei Klassenvariablen gibt es kein Objekt, dessen Komponente sie sein können. Man spricht sie darum über die Klasse an, z.B.:
Vektor::vz=0; // vz der Vektoren-Klasse auf 0 setzen
Ein etwas ausführlicheres Beispiel für die Verwendung dieser Vektor-Klasse ist:
class Vektor { ... wie oben ... };
...
void f () {
Vektor v;
cout << Vektor::vz << endl; // Ausgabe: 3
}
int main () {
cout << Vektor::vz << endl; // Ausgabe: 0
Vektor v1, v2 (1.0, 2.5);
cout << Vektor::vz << endl; // Ausgabe: 2
f();
cout << Vektor::vz << endl; // Ausgabe: 2
}
In diesem Beispiel wird während des Programmlaufs viermal die Zahl der gerade existierenden Vektoren
ausgegeben.
class Vektor {
public:
Vektor ();
Vektor (float, float);
~Vektor ();
int wieViele ();
private:
float x, y;
static int vz;
};
int Vektor::vz = 0;
Vektor::Vektor () { x=0; y=0; ++vz; }
Vektor::~Vektor () { --vz; }
Vektor::Vektor (float a, float b) { x=a; y=b; ++vz; }
int Vektor::wieViele () { return vz; }
void f () {
Vektor v;
cout << v.wieViele() << endl; // Ausgabe: 3
}
int main () {
// NICHT MOEGLICH --> Ausgabe: 0
Vektor v1, v2 (1.0, 2.5);
cout << v1.wieViele() << endl; // Ausgabe: 2
f();
cout << v1.wieViele() << endl; // Ausgabe: 2
}
Der Aufruf der Methode wieViele ist an ein Objekt gekoppelt. Das ist in gewisser Weise unnatürlich.
Die aktuelle Zahl der Vektoren ist nichts, was etwas mit einem bestimmten Vektor zu tun hat. Es ist sogar
unmöglich festzustellen, dass es aktuell gar keinen Vektor gibt. Es sollte also auch Methoden geben, die
zur gesamten Klasse, statt zu einem einzelnen Objekt gehören.
class Vektor {
public:
Vektor ();
Vektor (float, float);
~Vektor ();
static int wieViele (); // Deklaration statische Methode
private:
float x, y;
static int vz;
};
int Vektor::vz = 0;
... etc. wie oben ...
int Vektor::wieViele () { return vz; } // Definition statische Methode
// entspr. der "normaler" Methoden
void f () {
Vektor v;
cout << v.wieViele() << endl; // Ausgabe: 3
}
int main () {
cout << Vektor::wieViele() << endl; // Ausgabe: 0 Aufruf statische Methode
Vektor v1, v2(1.0, 2.5);
cout << Vektor::wieViele() << endl; // Ausgabe: 2
f();
cout << Vektor::wieViele() << endl; // Ausgabe: 2
}
Man beachte den Aufruf der Methode wieViele ohne den Selektionspunkt aber mit der
Bereichsangabe Vektor::. Damit kommt zum Ausdruck, dass wieViele zur Klasse Vektor gehört,
aber nicht in Abhängigkeit von einem bestimmten Objekt ausgewertet wird.
Statische Methoden haben keinen Zugriff auf ein Objekt. Sie verhalten sich damit wie freie Funktionen. Im Gegensatz zu diesen haben sie - als Bestandteile ihrer Klassen - aber Zugriff auf deren private Komponenten.
class Menge {
...
Menge operator+ (Menge); // Methode (und Operator)
static Menge vereinige (Menge, Menge); // statische Methode
...
};
Menge operator+ (Menge, Menge); // freie Funktion (und Operator)
Die wesentlichen Eigenschaften und Unterschiede von Methoden, statischen Methoden und freien Funktionen sind:
const float pi = 3.1415;
Komponenten einer Klasse können mit dem Schlüsselwort const ebenfalls als ``konstant'' erklärt werden. Konstante Datenkomponenten verhalten sich genau wie Konstanten auf Programmebene: sie sind unveränderlich. Wollen wir beispielsweise, dass die Registriernummer eines Buchs nicht verändert werden kann, dann erklären wir sie als konstant:
class Buch {
public:
Buch (string, string);
...
private:
const int regNr; // konstante Datenkomponente
...
};
Jedes Buch hat damit seine eigene unveränderliche Registriernummer regNr.
Man beachte den Unterschied von const und static. Das Schlüsselwort const erklärt Komponenten als unveränderlich. Die Komponente existiert einmal pro Objekt, kann aber nicht modifiziert werden. Mit static deklariert man Komponenten als klassenspezifisch. Die Komponente existiert einmal für die ganze Klasse, ist aber veränderlich.
static und const können natürlich auch kombiniert werden. Die Komponente ist dann sowohl klassenspezifisch als auch unveränderlich.
const float pi \\ Konstante
= 3.1415; \\ Initialisierer
class Buch {
public:
Buch (string, string);
...
private:
string titel, autor;
const int regNr;
static int naechsteNr;
};
Buch::Buch(string p_autor, string p_titel)
: regNr (naechsteNr) // Initialisierer
{
autor = p_autor;
titel = p_titel;
++naechsteNr;
}
Konstante Klassenkomponenten können nur mit Initialisierern belegt werden. Umgekehrt
kann jede Datenkomponente - nicht nur die konstanten - mit einem Initialisierer belegt werden:
Buch::Buch(string p_autor, string p_titel)
: regNr (naechsteNr++), // Initialisierer fuer alle Komponenten
titel (p_titel),
autor (p_autor)
{} // Rumpf des Konstruktors jetzt leer
Nicht-konstante Komponenten können (und sollten), konstante Komponenten müssen mit einem
Inititialisierer initialisiert werden!
class Vektor {
public:
Vektor ();
Vektor (float, float);
float xWert () const; // Konstante Methoden
float yWert () const; // veraendern ihr Objekt nicht
private:
float x, y;
};
...
float Vektor::xWert () const { return x; }
float Vektor::yWert () const { return y; }
Die Methoden xWert und yWert verändern das Objekt nicht, es sind konstante
Methoden.
Wird eine Methode als konstant erklärt, dann kann sie auch aktiviert werden, wenn das Objekt zu dem sie gehört selbst konstant ist. Beispiel:
const Vektor null(0.0, 0.0); // konstantes Objekt cout << null.xWert() << endl; // Aufruf konstanter Methoden erlaubtEs ist guter Stil alle Datenkomponenten als privat zu erklären und, soweit notwendig, mit speziellen Lese- und/oder Schreib-Methoden zu versehen. Die Leseoperationen sollten dann wie im Beispiel oben konstant sein.
Das Versprechen den Parameter nicht zu verändern ist natürlich nur interessant, wenn eine Veränderung, die innerhalb der Funktion erfolgt, auch eine Wirkung nach außen hätte. Das ist nur bei Referenzparametern möglich. Von Wertparametern werden ja lokale Kopien erzeugt, deren Modifikation außerhalb der Funktion irrelevant ist. Konstante Wertparameter sind darum unsinnig.
Konstante Referenzparameter sind dagegen sinnvoll. Sie stellen eine wichtige Alternative zu Wertparametern dar. Wie bei Wertparametern ist der Aufrufer sicher, dass seine aktuellen Parameter nicht verändert werden. Es wird allerdings bei der Übergabe keine vollständige Kopie erzeugt, sondern lediglich eine Referenz übergeben. Die Parameterübergabe hat damit die Effizienz der Übergabe per Referenz und die Sicherheit der Übergabe per Wert. Beispiel:
Vektor operator+ (const Vektor &v1, const Vektor &v2) {
return Vektor (v1.xWert() + v2.xWert(),
v1.yWert() + v2.yWert());
}
Beim Aufruf von operator+ werden jetzt zwei Referenzen übergeben, statt zwei
Objekte vom Typ Vektor vollständig neu zu erzeugen und mit Kopien zu belegen. Der Aufrufer kann aber
trotz der Referenzübergabe sicher sein, dass die aktuellen Parameter nicht modifiziert werden.
Konstante Objekte dürfen nur als Wert- oder konstante Referenzparameter übergeben werden:
Vektor f (Vektor v) { ... } // Wertparameter
Vektor g (Vektor &v) { ... } // Referenzparameter
Vektor h (const Vektor &v) { ... } // konstanter Referenzparameter
...
const Vektor null (0.0, 0.0);
f(null); // OK f erzeugt eine Kopie von null,
// diese darf veraendert werden
g(null); // FEHLER g erhaelt Zugriff auf null,
// verspricht aber nicht es unveraendert zu lassen
h(null); // OK h erhaelt Zugriff auf null und
// verspricht es unveraendert zu lassen
Man beachte, dass der Inhalt der Funktionen hier völlig unerheblich ist. Auch wenn g
seinen Parameter nicht verändert, die Übergabe eines konstanten Parameters an g ist verboten.
class Vektor {
public:
Vektor ();
Vektor (float, float);
Vektor operator+ (const Vektor &v) const;
private:
float x, y;
};
...
Vektor Vektor::operator+ (const Vektor &v) const {
return Vektor (x+v.x, y+v.y);
}
Das erste const sagt, dass der rechte Operand der Addition nicht verändert wird,
das zweite const garantiert, dass der linke Operand der Addition nicht verändert wird.
Konstante Objekte und Klassen-Komponenten erhöhen die Sicherheit durch Schutz vor versehentlichem Überschreiben. Sie machen zudem die Intentionen des Programm-Autors klar und erhöhen so die Lesbarkeit des Programms. Warnungen und Fehlermeldungen zu const sind stets ein Anzeichen, dass in der Konzeption des Programms etwas nicht in Ordnung ist: Etwas ist entweder nicht wirklich konstant, oder aber etwas, das konstant sein sollte, wird irrtümlicherweise verändert.
class S {
int y;
public:
int x;
int f (S);
private:
int g (S);
};
int S::f (S ps) {
x = 1;
y = 2;
ps.x = 1;
ps.y = 2;
return ps.g(ps)
+ ps.y;
}
int S::g (S ps) {
x = 1;
y = 2;
ps.x = 1;
ps.y = 2;
return ps.f(ps)
+ ps.y;
}
int h (S ps) {
ps.x = 1;
ps.y = 2;
return ps.f(ps)
+ ps.g(ps);
}
class C {
public:
C (int x) { c=x; }
void incA () { ++a; }
static void incB () { ++b; }
private:
int a = 0;
static int b = 1;
const int c;
};
int main () {
C c;
c.incA();
c.incB();
}
class Vektor {
public:
Vektor ();
...
private:
float x, y;
};
class Punkt {
public:
Punkt ();
...
float entfernt_von (Punkt);
private:
Vektor pos;
};
...
float Punkt::entfernt_von (Punkt p) {
return sqrt ( (pos.x - p.pos.x)*(pos.x - p.pos.x)
+
(pos.y - p.pos.y)*(pos.y - p.pos.y));
}
Wie ist er zu korrigieren?
class S {
public:
S() : y(y+1) {} // y um 1 erhoehen
~S() : y(y-1) {} // y um 1 reduzieren
static void f () {
cout << x << ", " << y<< endl; // x und y ausgeben
}
private:
int x = 1; // x mit 1 initialisieren
static int y = 0; // y mit 0 initialisieren
};
Wie wird das jeweils Gemeinte - soweit es nicht schon korrekt ist - korrekt ausgedrückt?
class C {
public:
static int i;
};
int C::i = 1;
Müssen sie darum public sein, oder ist auch folgendes erlaubt:
class C {
private:
static int i;
};
int C::i = 1;
Was ist mit
class C {
private:
static int i;
};
int main () {
int C::i = 1;
...
}
class Menge {
public:
Menge (); // leere Menge
Menge (int); // ein-elementige Menge
Menge operator+ (const Menge &) const; // Vereinigung
Menge operator* (const Menge &) const; // Schnitt
bool istEnthalten (int) const; // ist Element
private:
void fuegeEin (int i); // interne Hilfsfunktion
void entferne (int i); // interne Hilfsfunktion
int m[10];
int a;
};
//Test:
int main () {
Menge m1, m2(2);
Menge m3, m4;
m1 = m1 + Menge(2);
m1 = m1 + Menge(3);
m2 = m2 + Menge(3);
m2 = m2 + Menge(4);
m3 = m1 + m2;
m4 = m1 * m2;
}
Menge operator+ (const Menge) const;
Menge operator+ (const Menge &) const;
Menge operator+ (Menge) const;
Menge operator+ (const Menge &) const;
Was sind die Vor- und Nachteile?
class Menge {
public:
Menge (); // leere Menge
Menge (int); // ein-elementige Menge
Menge operator+ (const Menge &) const; // Vereinigung
Menge operator* (const Menge &) const; // Schnitt
bool istEnthalten (int i) const; // ist Element
private:
void fuegeEin (int i);
void entferne (int i);
int m[10];
int a;
};
als befeundete freie Operatoren.
class A {
public:
A(int p) : x(p) {}
private:
int x;
};
class B {
public:
B(A a) { y = a; }
private:
A y;
};
dagegen aber
class A {
public:
A(int p) : x(p) {}
private:
int x;
};
class B {
public:
B(A a) : y(a) {}
private:
A y;
};
korrekt? Was unterscheidet die beiden Programmstücke?
Geben Sie eine Variablendefinition für eine Variable b vom Typ B an und erläutern Sie die Reihenfolge und das Ergebnis der Initialisierung, je nach dem ob b global oder lokal in einer Funktion definiert wird.
class X {
public:
X () : f(0.0) {}
float f;
};
class C {
public:
X a;
float b;
};
int main () {
C c;
}
class X {
public:
X () : f(0.0) {}
float f;
};
class C {
public:
C (X x) : a(x) {}
X a;
float b;
};
int main () {
C c;
}
Ist es korrekt? Wenn nein: Warum nicht? Wenn ja: Welche Konstruktoren werden in welcher Reihenfolge aufgerufen?