next up previous contents index
Nächste Seite: Objektorientierte Programmierung: Entwurf von Aufwärts: Programmierung II C++ Vorherige Seite: Inhalt   Inhalt   Index

Unterabschnitte

Klassen


Klassen: Verbunde mit Geheimnissen

Klassen: Verbunde mit privaten und öffentlichen Komponenten

Klassen sind eine Variante der Verbunde (Structs). Umgekehrt sind Verbunde eine Sonderform der Klassen. Einem Verbund S, der als




$\textstyle \parbox{3cm}{
{\tt struct S \{\\ \makebox[2ex]{}T1 s1;\\ \makebox[2ex]{}T2 s2;\\ \makebox[2ex]{}...\\ \};}
}$ definiert wurde, entspricht die Klasse $\textstyle \parbox{4cm}{
{\tt class S \{\\ public:\\ \makebox[2ex]{}T1 s1;\\ \makebox[2ex]{}T2 s2;\\ \makebox[2ex]{}...\\ \};}
}$




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




$\textstyle \parbox{3cm}{
{\tt class K \{\\ \makebox[2ex]{}T1 s1;\\ \makebox[2ex]{}T2 s2;\\ \makebox[2ex]{}...\\ \};}
}$ definiert wurde, entspricht der Verbund $\textstyle \parbox{4cm}{
{\tt struct K \{\\ private:\\ \makebox[2ex]{}T1 s1;\\ \makebox[2ex]{}T2 s2;\\ \makebox[2ex]{}...\\ \};}
}$




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.

\fbox{
\begin{minipage}[t]{12cm}
In Klassen und Verbunden kann zwischen \uml {...
...e nicht explizit als \uml {o}ffentlich gekennzeichnet werden.
\end{minipage} }

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.


Verwendung von public und private

Die Klasse Vektor mit öffentlichem x und y




$\textstyle \parbox{3cm}{
{\tt class Vektor \{\\ public: \\ \makebox[2ex]{}float x, y;\\ \};}
}$ ist das Gleiche wie der Verbund $\textstyle \parbox{3.5cm}{
{\tt struct Vektor \{ \\ \makebox[2ex]{} float x, y;\\ \};}
}$




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
  };

Eingeschränkte Sichtbarkeit der privaten Komponenten einer Klasse

Nach so viel Formalismus wird es Zeit sich mit der Bedeutung von public und private zu beschäftigen. Die öffentlichen Komponenten einer Klasse verhalten sich in jeder Beziehung genau so wie die ``normalen'' Komponenten von Verbunden (struct-Typen). Die privaten Komponenten dagegen haben eine eingeschränkte Sichtbarkeit: sie können nur innerhalb der Klasse verwendet werden, zu der sie gehören. Beispiel:
  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.



\fbox{
\begin{minipage}[t]{12cm}
{\em Private Komponenten} d\uml {u}rfen nur in Methoden der Klasse benutzt werden, zu der sie geh\uml {o}ren.
\end{minipage} }



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!

Klassen, nicht Objekte, haben eine eigene Privatsphäre

public und private operieren auf der Ebene von Klassen, nicht auf der Ebene von Objekten. Mit private werden die Komponenten eines Objekts vor dem Zugriff von Funktionen und Methoden anderer Klassen geschützt. Die Methoden der Klasse, zu der ein Objekt gehört, haben vollen Zugriff auf die eigenen Komponenten und auf die Komponenten aller Objekte der gleichen Klasse. Solange sie zu gleichen Klasse gehören können also Objekte auf die privaten Komponenten anderer Objekte zugreifen. Beispiel:
  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.

\fbox{
\begin{minipage}[t]{12cm}
{\tt private} hei\ss{}t private Komponente {\...
...ternen Gebrauch von
allen Methoden aller Objekte der Klasse.
\end{minipage} }

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.


Kapselung: Klassen als Softwarekomponenten mit Schnittstelle und Implementierung

Öffentlich und privat als Schnittstelle und Implementierung

Private Klassenkomponenten mit ihrer beschränkten Sichtbarkeit unterscheiden sich von den Komponenten eines Verbunds nur dadurch, dass man weniger mit ihnen machen kann. Das neue Konzept der Klassen bringt also eine Einschränkung gegenüber den Verbunden! Diese Beschränkung soll die Entwicklung komplexer Softwaresysteme unterstützen. Mit ihr kann eine Klasse als Softwarekomponente mit klar definierter Funktionalität und Implementierung realisiert werden.

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:

Mit ``Benutzer'' ist dabei der Quellcode - und dessen Entwickler - gemeint, der nicht zur Klasse gehört, sondern sie nur benutzt.

Zwei Arten von Benutzern

Bei dem Wort ``Benutzer'' denkt man meist an einen Menschen, der ein fertiges (Software-) Produkt benutzt. Der Benutzer steht im Gegensatz zum Entwickler. Der eine stellt etwas her, der andere benutzt es.

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.

Schnittstelle und Implementierung

Teilt man Softwareprodukte in Komponenten auf, die sich gegenseitig benutzen, dann kommt man naturgemäß dazu, die einzelnen Komponenten weiter in Schnittstelle und Implementierung aufzuteilen. Für die kgv-Funktion oben (und deren Autorin) ist nur die Schnittstelle von ggt interessant: Was gebe ich wie hinein, damit was wie herauskommt. Die Implementierung - Schleife oder Rekursion - ist vom Standpunkt des Benutzers völlig unerheblich. Hauptsache es funktioniert, wie ist egal.

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):

Abbildung 1: Das Prinzip der Kapselung
\begin{figure}\makebox[\textwidth]{
\epsfbox{Figure/kapselung-0.eps}}
\end{figure}

Mengen-Typ als Verbund mit informaler Schnittstelle

Ein Verbund-Typ Menge zur Darstellung von Mengen positiver ganzer Zahlen könnte wie folgt aussehen:
  //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.

Mengen-Typ als Klasse mit expliziter Trennung von Schnittstelle und Implementierung

Mit dem Klassen-Konstrukt (mit der Aufteilung in öffentlich und privat) kann die Zweiteilung klar zum Ausdruck gebracht und (vom Compiler) überwacht werden. private entspricht dabei dem Deckel auf dem Lichtschalter, der den direkten Zugriff auf die Kontakte verhindert:
  // 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: Bei dieser Mengen-Klasse können Elemente nur auf dem offiziellen Weg über die entsprechende Methode eingefügt oder entnommen werden. Ein direkter Zugriff auf die Innereien ist nicht möglich. Der Deckel kann nicht abgeschraubt werden.

Klassendiagramm zur Darstellung einer Klasse

Softwarekonponenten werden der besseren Übersichtlichkeit halber oft grafisch dargestellt. Nach vielen Jahren des völligen Chaos' durch konkurrierende Darstellungskonventionen hat sich jetzt UML1 allgemein durchgesetzt. Die Klasse Menge wird in UML folgendermaßen dargestellt (siehe Abbildung 2):

Abbildung 2: Klassendiagramm der Menge
\begin{figure}\makebox[\textwidth]{
\epsfbox{Figure/klassenDia-0.eps}}
\end{figure}

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

Geheimnisse nutzen den Anwendern: Was ich nicht weiß, kann ich nicht missverstehen oder vergessen.

Die Kapselung, also die Trennung von Schnittstelle und Implementierung, wird oft auch als Geheimnisprinzip bezeichnet. Die Implementierung ist ein Geheimnis der Klasse. Sie geht keinen ihrer Benutzer etwas an.

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.

Geheimnisse nutzen den Implementierern: Was nur ich weis, brauche ich nicht abzusprechen oder zu erklären.

Auf die privaten Komponenten kann nur innerhalb der Methoden der Klasse selbst zugegriffen werden. Damit ist garantiert, dass sie jederzeit und ohne Absprache mit dem Benutzer der Klasse geändert, gestrichen oder durch gänzlich andere ersetzen werden können.

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.

Abbildung 3: Der Begriff der Kopplung
\begin{figure}\makebox[\textwidth]{
\epsfbox{Figure/kopplung-0.eps}}
\end{figure}


Kopplung von Softwarekomponenten

Unter Kopplung versteht man die Verzahnung von Softwarekomponenten die dadurch entsteht, dass das Wissen über das Wesen der einen Bestandteil der anderen ist. So ist der Benutzer der Klasse Menge an die Klasse Menge gekoppelt, weil er beispielsweise wissen muss, wie die Methode zum Einfügen eines Elementes heißt. Dieses Wissen ist im Quellcode des Benutzers ``fest verdrahtet'' und beide sind damit gekoppelt (Siehe Abbildung 3).

Abbildung 4: Zu enge Kopplung von Anwendung und Implementierung
\begin{figure}\makebox[\textwidth]{
\epsfbox{Figure/klasse-0.eps}}
\end{figure}

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.

private und public dienen dazu den Programmcode zu organisieren

Mit dem Schlüsselwort private wird das Aussehen von Programmtexten beeinflusst. Mit ihm sollen bestimmte Komponenten einer Klasse - nicht vor Menschen (!) -, sondern vor anderen Textstücken im gleichen Programm ``verborgen'' werden. Wobei ``verborgen'' nichts anderes heißt, als dass der Compiler eine Fehlermeldung ausgibt, wenn er einen ``verborgenen'' (privaten) Bezeichner an der falschen Stelle antrifft.

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.


Freunde

Freundschaft in C++

Freie Funktionen und Methoden anderer Klassen dürfen nicht auf die privaten Komponenten einer Klasse zugreifen. Ein Objekt ist damit vor den indiskreten Zugriffen ``fremder'' Objekte und Funktionen geschützt. Mit dem Konzept der Freundschaft kann dieses Prinzip aufgehoben werden.

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.

\fbox{
\begin{minipage}[t]{12cm}
{\em Freunde} einer Klasse sind Programmst\um...
... private}--Beschr\uml {a}nkungen
dieser Klasse nicht gelten.
\end{minipage} }


Funktionen als Freunde

Im folgenden Beispiel erklärt die Klasse Vektor die freie Funktion void drucke(Vektor) zu ihrem Freund:
  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.


Klassen als Freunde

Wird eine Klasse zum Freund einer anderen erklärt, dann bedeutet dies, dass alle Methoden des Freundes Zugriff erhalten. Selbstverständlich erklärt man nur Klassen zu Freunden, die eng zusammenarbeiten. Beispiel:
  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.


Überladene Operatoren

Überladene Operatoren als freie Funktionen

Operatoren können bekanntlich mit neuen Definitionen belegt werden. Beispielsweise kann die Addition von Vektoren als Operator definiert werden:
  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);

zu interpretieren.

Überladene Operatoren in Klassen

Statt als freie Funktionen können Operatoren auch als Methoden definiert werden. Das ist natürlich besonders interessant, wenn sie auf private Komponenten zugreifen müssen, was für eine freie Funktion (ohne Freundschaft) ja nicht möglich ist.

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.

Unäre Operatoren

Unäre Operatoren können als Methode definiert werden:
  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.

Inline Funktionen und Methoden

Inline Funktionen

Eine einfache Funktion wie etwa
  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.

Inline Methoden

Methoden können natürlich ebenfalls als inline deklariert werden. Beispiel:
  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.

Inline Methoden innerhalb der Klassendefinition

Inline Methoden können direkt in die Klassendefinition platziert werden. Beispiel:
  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.

Konstruktoren und Destruktoren

Methoden zur Initialisierung

Der Klasse Menge von oben fehlt noch eine Initialisierungsmethode mit der ein Mengenobjekt in einen definierten Anfangszustand versetzt werden kann. Typischerweise wird man eine Menge als leere Menge initialisieren und die Initialisierungs-Methode init nennen:
  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();
  ...

Konstruktoren sind Initialisierungs-Methoden mit Compiler-Unterstützung

Die Initialisierung ist eine Routineaktion. Als solche kann und sollte sie vom Compiler übernommen werden. Gibt man der Initialisierungs-Methode einer Klasse den Namen der Klasse und lässt den Ergebnistyp weg, dann erkennt der Compiler sie als Konstruktor, d.h. als Initialisierungsroutine für deren Aktivierung er verantwortlich ist.
  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.

\fbox{
\begin{minipage}[t]{12cm}
Konstruktoren sind Initialisierungsroutinen f...
...atisch an der Stelle der {\em Variablendefinition aufgerufen}.
\end{minipage} }

Der Compiler erzeugt keine Konstruktordefinitionen

Manche Klassen enthalten keinen Konstruktor. Das ist erlaubt. Klassen müssen keinen Konstruktor enthalten. Der Compiler erzeugt auch keine Konstruktoren automatisch. Objekte einer Klasse ohne Konstruktor werden wie Variablen jedes beliebigen Typs behandelt: globale Objekte werden mit 0 initialisiert und alle anderen enthalten Zufallswerte. Beispiel:
  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.

Konstruktoren mit Argumenten

Konstruktoren können Argumente haben. Für eine Klasse können auch beliebig viele Konstruktoren definiert werden, wenn sie nur an Hand ihrer Parameterliste unterschieden werden können. Die Konstruktoren sind dann überladene Funktionen. Eine Menge kann beispielsweise mit keinem, einem oder zwei Elementen initialisiert werden:
  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);


Konstruktor als Konversionsoperation

Konstruktoren mit einem Argument werden bei Bedarf als Konversionsoperation behandelt. Dazu werden sie in dieser Funktion explizit über den Typnamen aufgerufen:

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.

explicit: der Konstruktor wird nur explizit aktiviert

Mit Schlüsselwort explicit vor der Konstruktordefinition kann die implizite Verwendung dieses Konstruktors als Konversionsfunktion verhindert werden. Beispiel:
  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.

Default-Konstruktor: Konstruktor der ohne Argumente aktiviert wird

Ein Konstruktor ohne Argumente wird Default-Konstruktor genannt. Achtung: er heißt nicht etwa so, weil der Compiler ihn automatisch erzeugt, sondern weil er vom Compiler automatisch (``per default'') aufgerufen wird, wenn keine Argumente den Einsatz eines anderen Konstruktors vorschreiben!

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.

Klassen ohne Default-Konstruktor

Eine Klasse muss nicht unbedingt einen Default-Konstruktor definieren. Wenn er benötigt wird, dann muss er aber vorhanden sein:
  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.

Initialisierungslisten

Verbunde können durch Initialisierungslisten mit einem Wert belegt werden. Verbunde sind Klassen deren Komponenten alle öffentlich sind. Klassen, die nur öffentliche Datenkomponenten und keinen Konstruktor enthalten, können darum wie Verbunde mit einer Initialisierungsliste intialisiert werden:
  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.

Destruktoren

Destruktoren sind komplementär zu Konstruktoren. Sie werden aufgerufen, bevor ein Objekt vernichtet wird. Beispiel:
  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.


Initialisierung von Objekten, Initialisierer

Initialisierung von einfachen Variablen

Variablen können bei ihrer Definition mit einen ersten Wert belegt werden. Bei skalaren Typen und Feldern sieht das wie eine Zuweisung aus, z.B:

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.

Initialisierungsreihenfolge

Klassen enthalten üblicherweise Datenkomponten. Diese können wiederum Objekte einer Klasse sein, die selbst wieder Objekte als Komponenten enthält, etc. Ein Objekt kann also aus beliebig tief verschachtelten Unterobjekten bestehen. Bei der Initialisierung des Gesamtobjekts werden auch die Unterobjekte initialisiert. Die Reihenfolge der Initialisierung ist dabei: Diese Regel gilt rekursiv für alle Objekte, Unterobjekte, Unter-Unterobjekte, etc. Die Regeln der Initialisierung ergeben sich immer aus dem Typ der Objekte und Unterobjekte. Das Vorgehen ist intuitiv einsichtig: zuerst werden die Einzelteile fertig gemacht, dann wird aus ihnen ein Ganzes konstruiert. Der Konstruktor ist für das Gesamte zuständig.

Beispiel Initialisierungsreihenfolge

Als Beispiel betrachten wir wieder einmal Vektoren:
  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:

  1. g.o.x wird initialisiert (Unter-Unterobjekt)
  2. g.o.y wird initialisiert (Unter-Unterobjekt)
  3. g.o.Vektor() wird ausgeführt (Konstruktor Unterobjekt)
  4. g.r.x wird initialisiert (Unter-Unterobjekt)
  5. g.r.y wird initialisiert (Unter-Unterobjekt)
  6. g.r.Vektor() wird ausgeführt (Konstruktor Unterobjekt)
  7. g.Gerade() wird ausgeführt (Konstruktor Objekt).

Das Prinizip, das die Initialisierungsreihenfolge bestimmt, ist insgesamt sehr einfach:



\fbox{
\begin{minipage}[t]{12cm}
Objekte werden {\em von innen nach aussen} in...
...rs f\uml {u}r das Objekt das diese Komponenten enth\uml {a}lt.
\end{minipage} }

Redundante Initialisierung

Man beachte dass im letzten Beispiel die Komponenten prinzipiell zweimal mit Werten belegt werden. Zuerst durch ihren eigenen Konstruktor und dann durch eine Zuweisung im Konstruktor der Klasse deren Teilkomponente sie sind. Beispielsweise wird g.r zuerst durch den Defaultkonstruktor von Vektor

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.

Initialisierer

Initialisierer können in Konstruktoren verwendet werden, um die Initialisierung der Unterkomponenten zu steuern. Mit ihnen kann die oben angesprochene doppelte Initialisierung einer Komponente vermieden werden. Beispiel:
  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:
  1. g.o.x wird initialisiert
  2. g.o.y wird initialisiert
  3. g.o.Vektor(0.0, 0.0) wird ausgeführt
  4. g.r.x wird initialisiert
  5. g.r.y wird initialisiert
  6. g.r.Vektor(1.0, 1.0) wird ausgeführt
  7. g.Gerade() wird ausgeführt
Die Zuweisungen in g.Gerade() können allerdings wegfallen. Üblicherweise initialisiert man alle Komponenten durch Initialisierer, auch wenn es sich nicht um Klassen handelt:
  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.

Klassen mit Klassenkomponenten und automatisch generierte Default-Konstruktoren

Bisher haben wir immer betont, dass Konstruktoren im Allgemeinen und der Default-Konstruktor im Besonderen nicht vom Compiler automatisch erzeugt werden, sondern explizit definiert werden müssen. Von dieser Regel gibt es eine Ausnahme: Wenn der Typ einer Komponente eine Klasse mit Default-Konstruktor ist, dann wird der Konstruktor der ``Unterklasse'' immer aktiviert; auch dann, wenn die Klasse selbst keinen Konstruktor hat:
  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

Bei Klassen mit Klassen als Komponenten werden die Default-Konstruktoren der Subklassen auch dann aktiviert, wenn die Klasse selbst keinen Konstruktor hat. Dies setzt aber die Regeln über die notwendige Existenz eines Default-Konstruktors nicht außer Kraft.


Statische Komponenten einer Klasse

Statische Datenkomponenten (Klassenvariablen)

Komponenten - Daten und Methoden - einer Klasse können mit dem Schlüsselwort static als statisch deklariert werden. Die entsprechende Komponente ist dann klassen-spezifisch statt objekt-spezifisch. Bei statischen Daten-Komponenten bedeutet dies, dass die Komponente nur einmal für alle Instanzen (Objekte) der entsprechenden Klasse angelegt wird.

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:

Verwendung statischer Komponenten

Normale Datenkomponenten werden über die Punktnotation angesprochen, z.B.

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.

Private Klassenvariablen

Klassenvariablen dürfen natürlich auch privat sein. In dem Fall müssen sie über Methoden angesprochen werden:
  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.

Statische Methoden

Neben statischen Datenkomponenten gibt es auch statische Methoden. Wie die statischen Datenkomponenten gehören statische Methoden nicht zu einzelnen Objekten sondern zu der Klasse insgesamt. Sie werden darum unabhängig von einem bestimmten Objekt ausgeführt. Beispielsweise kann die Angabe der Zahl der aktuell existierenden Vektoren als statische Methode definiert werden:
  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.

Methoden, statische Methoden und freie Funktionen

Methoden, statische Methoden und freie Funktionen bieten ähnliche Möglichkeiten. Betrachten wir dazu noch einmal das Mengenbeispiel mit drei Varianten der Vereinigung:
  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:


Konstante Komponenten, konstante Parameter

Konstante Datenkomponenten

Wir erinnern uns, dass mit const Konstanten im Programm definiert werden können. Beispiel:

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 und Initialisierer

Da Zuweisungen an Konstanten nicht möglich sind, können sie ihren Wert nur durch einen Initialisierer erhalten. Bei Konstanten auf Programm- oder Funktionsebene wird der Initialisierer direkt zur Konstanten geschrieben:
  const float pi         \\ Konstante
              = 3.1415;  \\ Initialisierer

Initialisierer von Klassenkomponenten

Konstante Klassenkomponenten müssen ebenfalls durch einen Initialisierer mit einem Wert belegt werden. Sie werden beim Konstruktor in der speziellen Notation der Initialisierer angegeben:
  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!

Konstante Methoden

Eine Methode kann wie eine Datenkomponente als konstant erklärt werden. Hier ist die Bedeutung allerdings nicht ``diese Komponente ist unveränderlich'', sondern - da Methoden selbst etwas Aktives sind - : ``diese Methode verändert nichts!''. Beispiel:
  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 erlaubt
Es 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.

Konstante Parameter

Auch die formalen Parameter einer Funktion (freie Funktion oder Methode) können als konstant erklärt werden. Die Funktion sagt damit: Ich werde diesen Parameter nicht verändern!.

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.

Konstante Methoden mit konstantem Parameter

Die Kombination konstante Methode mit konstantem Parameter wird häufig eingesetzt:
  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.

const oder nicht const

Das Schlüsselwort const kann zu einer Flut von Fehlermeldungen und Warnungen führen. Es wird trotzdem empfohlen mit Konstanten, konstanten Parametern und konstanten Methoden zu arbeiten und gegebenenfalls allen Warnungen und Fehlermeldungen nachzugehen.

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.

Philosophie einer Const-Definition

Die unterschiedlichen Bedeutungen, die mit const ausgedrückt werden, sind: Der Einsatz von const ist wie der von static letztlich eine ``philosophische Frage''. Alle Elemente der Klassendefinition müssen zusammenpassen und insgesamt ein möglichst einfach benutzbares, konsistentes, logisches - kurz ``schönes'' - Konstrukt ergeben.

Übungen

Aufgabe 1

  1. Definieren Sie als äquivalente Klasse: struct Vektor { float x, y; };
  2. Ist struct S { ... private: ... }; erlaubt. Wenn ja: was bedeutet es?
  3. Welche Zugriffe auf Klassen-Komponenten sind in folgendem Programm erlaubt, welche sind nicht erlaubt:
      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);
      }
    
  4. Kann jedes Programm, in dem Klassen vorkommen, in ein äquivalentes Programm ohne Klassen umgesetzt werden? (Äquivalent: Bei gleicher Eingabe wird die gleiche Ausgabe erzeugt.) Wenn nein, geben Sie ein Beispiel an, bei dem die Umsetzung nicht möglich ist. Wenn ja, wozu gibt es denn überhaupt Klassen?
    1. Kann eine Funktion ein Freund einer Klasse sein?
    2. Kann eine Klasse ein Freund einer Funktion sein?
    3. Kann eine Klasse ein Freund einer Klasse sein?
  5. Welche Fehler enthält folgende Klassendefinition. Wie wird das Gewollte korrekt formuliert?
      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();
      }
    
  6. Welchen Fehler enthält:
      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?
  7. Welche Fehler enthält:
      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?
  8. Klassenvariablen müssen außerhalb der Klasse definiert werden:
      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; 
        ... 
      }
    
  9. Komplexe technische Systeme sind aus Komponenten aufgebaut, die jeweils eine Schnittstelle und eine Implementierung haben. Nennen Sie Beispiele und erläutern Sie wie dieses Prinzip auf Softwareprodukte übertragen wird.
  10. Erläutern Sie den Begriff der Kopplung am Beispiel einer Mengen-Klasse und ihrer Benutzer.

Aufgabe 2

  1. Erläutern und begründen Sie jeden Einsatz von const in folgender Definition:
      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;
      }
    
  2. Begründen Sie die Klassifikation der Methoden und Datenkomponenten in öffentlich und privat.
  3. Warum ist das erste const in folgender Deklaration unsinnig, das zweite jedoch nicht?

    Menge operator+ (const Menge) const;

  4. Wieso ist das erste const in folgender Deklaration nicht unsinnig?

    Menge operator+ (const Menge &) const;

  5. Wodurch unterscheiden sich die beiden Methoden:

    Menge operator+ (Menge) const;
    Menge operator+ (const Menge &) const;

    Was sind die Vor- und Nachteile?

  6. Ergänzen Sie die Klasse Menge um die Definition der Methoden. Benutzen Sie dabei soweit wie möglich Initialisierer.
  7. Formulieren Sie das Beispiel so um, dass Vereinigung und Schnitt freie Operatoren sind. Die privaten Komponenten dürfen dabei nicht öffentlich gemacht werden!
  8. Formulieren Sie das Beispiel so um, dass Vereinigung und Schnitt statische Methoden sind.
  9. Definieren Sie die Ausgabe von Mengen in drei Varianten:
    1. Eine Ausgabe-Methode.
    2. Eine freie Ausgabe-Funktion.
    3. Eine statische Ausgabe-Methode.
  10. Definieren Sie die Operatoren ``+'' und ``-'' in:
      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.

Aufgabe 3

Konten haben einen Besitzer, einen Wert und einen Zinssatz. Der Zinssatz ist für alle Konten gleich: 3 Prozent für Guthaben, 5 Prozent für Kredite (= negative Guthaben). Von einem Konto zum anderen können beliebige Beträge überwiesen werden. Das Guthaben bzw. der Kredit eines Kontos kann für einen bestimmten Zeitraum verzinst werden; das Guthaben wird dabei entsprechend verändert. Definieren Sie die Klasse der Konten mit geeigneten Methoden und/oder Funktionen.

Aufgabe 4

  1. Was versteht man unter einem Konstruktor und was ist der Default-Konstruktor?
  2. Stimmt es, dass ...
    1. eine Klasse immer mindestens / genau / höchstens einen Konstruktor haben muss?
    2. eine Klasse immer einen Default-Konstruktor haben muss?
    3. der Compiler einen Default-Konstruktor erzeugt, wenn für eine Klasse keiner definiert wurde.
    4. Objekte immer mit einem Konstruktor initialisiert werden?
    5. Objekte, deren Klasse mindestens einen Konstruktor hat, immer mit einem Konstruktor initialisiert werden.
    6. Klasssen mit Konstruktor auch einen Destruktor haben müssen?
    7. Klassen mit Destruktor auch einen Konstruktor haben müssen?
  3. Welche Methoden dürfen Initialisierer enthalten?
  4. Unter welchen Umständen muss ein Initialisierer vorhanden sein?
  5. Was ist an folgender Definition falsch:
      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.

  6. Ist folgendes Programmstück korrekt? Wenn nein: warum nicht? Wenn ja: Wird der Konstruktor von X aktiviert?
      class X {
      public:
        X () : f(0.0) {}
        float f;
      };
    
      class C {
      public:
        X       a;
        float   b;
      };
    
      int main () {
        C c;
      }
    
  7. Was ist mit
      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?
  8. Geben Sie ein Beispiel an, für den (seltenen) Fall eines Compiler-erzeugten Default-Konstruktors.
  9. Eine Klasse erklärt eine andere zum Freund. Welche Auswirkung hat dies auf die Ausführung der Konstruktoren?


next up previous contents index
Nächste Seite: Objektorientierte Programmierung: Entwurf von Aufwärts: Programmierung II C++ Vorherige Seite: Inhalt   Inhalt   Index
2002-09-10