C++ - HalloWelt Playgames Getfun

Direkt zum Seiteninhalt

Hauptmenü:

Wissen > Programmiersprachen

Bei diesem Crashkurs war ich bemüht, jedes überflüssiges Wort wegzulassen (natürlich auch die Witze hihi). Also: "Seven of Nine" (s. Voyager) is watching you und sagt unerbittlich:   "... ist irrelevant". Ja ja, und deswegen ist dies auch etwas schwer zu verstehen, aber nicht unmöglich (hoffe ich doch sehr). Wenn Du hierdurch auf den Weg gebracht wirst und Dir neugierg ausführliche Lektüre suchst, habe ich bestimmt schon eine Menge erreicht.Oder? Also: Nicht den Kopf hängen lassen, wegen dieser kurzen Ausführung.

Einführung in Klassen und Objekte, Vererbung  und Polymorphie

 

Die Klasse ist ein Sprachelement, um modulare, wartbare Programme zu schreiben.

  • Anwenderdefinierte Typen in C

Eine C++ Klassendeklaration ist eine Weiterentwicklung der C-Strukturdeklaration. Beispiel einer C-Strukturdeklaration:

struct xy { double dX; int dY;};

Es können nun Instanzen angelegt werden, z.B.:

struct xy topLeft = {0.0, 0.0};

Die gleiche Strukturdeklaration mit typedef-Syntax lautet:

typedef struct xy { double dX; int dY;} XY;

Instanzen dazu, z.B.:

XY topLeft = {0,0};

Die Instanzen von XY - also topLeft - entsprechen C++-Objekten. Jedes verbraucht auf dem Stapel Speichermenge und läßt sich mit einfachen Zuweisungen benutzen,z.B.:

bottomRight = topLeft;

Für den C-Compiler nicht verständlich :

bottomRight = topLeft + 1;

 

  • Übergang nach C++

Das obige Beispiel läßt sich in der einfachsten Form der Klassendeklaration so codieren:

class XY{

public:
    double m_dX;
    double m_dY;

};

Das Schlüsselwort public ermöglicht den direkten Zugriff auf m_dY und m_dY, als wären die Datenelemente gewöhnliche Strukturelemente. Das Präfix m_ ist übliche Konvention bei der Benennung von Datenelementen in C++ Klassen (d.h. keine zwingend notwendige Syntax).

Ein XY-Objekt entsteht durch folgenden Code:

XY bottomRight;                    //ein nicht initialisiertes Objekt
bottomRight.m_dX = 1.0;
bottomRight.m_dY = 1.0;

Die Klassenelemente m_dX und m_dY haben ihre Werte einzeln erhalten, weil der C++-Compiler in den meisten Klassen keine Anweisund der folgenden Art akzeptiert:

XY bottomRight = { 1.0, 1.0 };   // Fehler, wenn kein passender Klassenkonstruktor implementiert ist

Auch in C++ gibt es Strukturen. Statt eine XY-Klasse zu deklarieren können Sie eine C++ Struktur XY deklarieren:

struct XY   {
double m_dX;
double m_dY;   };

In C++ ist der Unterschied zwischen einer Klasse und einer Struktur, dass die Strukturelemente standardmäßig öffentlich zugänglich sind.

 

  • Konstruktoren

C++-Klassen können im Gegensatz zu C-Strukturen auch Funktionen aufnehmen. Diese Elementfunktionen haben den Zugriff auf alle Datenelemente einer Klasse. Zudem gibt es in jeder C++-Klasse spezialisierte Elementfunktionen mit der Aufgabe, die Objekte zu initialisieren. Sie heißen Konstruktoren. Wird von Ihnen kein Konstruktor definiert, generiert der C++-Compiler einen Standardkonstruktor ohne Argumente. Der Standardkonstruktor weist dem Klassenobjekt Speicherplatz zu, initialisiert aber nicht diejenigen Datenelemente, die Standardtypen haben.

Beispiel einer Funktion, bei der der Standardkonstruktor aufgerufen wird:

void func( ) {

XY bottomRight; //Das XY-Objekt bottomRight wird auf dem Stapel angelegt.
                            //Die Datenelemente m_dX und m_dY bleiben uninitialisiert

}

Beispiel eines Konstruktors, der inline definiert ist:

class XY{

public:
    double m_dX, double m_dY;
    XY(double dX, double dY) { m_dX = dX; m_dY = dY; }

};

Jetzt akzeptiert der Compiler die Anweisung

XY bottomRight = { 1.0, 1.0 }; 

Da nun ein eigener Konstruktor geschrieben wurde, wird kein Standardkonstruktor mehr generiert und somit wird die Zeile

XY bottomRight;  

nicht mehr angenommen.

Man kann zusätzlich auch einen eigenen Standardkonstruktor schreiben, der die Datenelemnete auf null setzt. Dann gibt es zwei Konstruktoren in der Klassendeklaration:

class XY{

public:
   double m_dX, double m_dY;  
    XY( ) { m_dX = 0.0; m_dY = 0.0; }
    XY(double dX, double dY) { m_dX = dX; m_dY = dY; }

};

Der Compiler unterscheidet die gleichnamigen Funktionen anhand der Parameter. Jetzt sind die beiden Zeilen

XY bottomRight;
XY bottomRight(1.0, 1.0);  

zulässig.

 

  • Destruktoren

Ein Destruktor ist eine weitere spezialisierte C++-Funktion. In der Regel hat der Destruktor die Aufgabe, den Speicherabschnitt, den das Objekt oder Datenelement belegt, freizugeben. Der Name dieser Funktion besteht aus dem Namen der Klasse und einer vorangestellten Tilde (~). Jede Klasse hat eine (und nur eine) Destruktorfunktion. Diese Funktion hat keine Argumente und liefert keinen Ergebniswert. Der Destruktor wird automatisch für globale Objekte und solche, die auf dem Stapel liegen, aufgerufen, sobald das Objekt den Gültigkeits(Sichbarkeits-)bereich verläßt. Wir brauchen zwar keine expliziten XY-Destruktor, schreiben ihn aber zur Übung und zwar diesmal nicht inline:

class XY{

public:
   double m_dX, double m_dY;  
    XY( );                                          //Standardkonstruktor
    XY(double dX, double dY); // expliziter Konstruktor
     ~XY( );                                     //Destruktor

};

XY::XY( )
{

    m_dX = m_dY = 0.0;

}

XY::XY(double dX, double dY )
{

    m_dX =dX;
    m_dY = dY;

}

XY::~XY(double dX, double dY )
{

}

Der Präfix XY:: sagt dem Compiler, dass es sich um Elementfunktionen der Klasse XY handelt.

 

  • Andere Elementfunktionen

Ihre speziellen Fähigkeiten erhält eine Klasse durch Elementfunktionen. Ein Beispiel als Inline-Version:

class XY{

public:
   double m_dX, double m_dY;  
    XY( );                                          //Standardkonstruktor (spezialisierte Elementfunktion)
    XY(double dX, double dY); // expliziter Konstruktor (spezialisierte Elementfunktion
    double Getx( ) const {return m_dX;}  //  Elemenfunktion
    double Gety( ) const {return m_dY;}  //  Elementfunktion

};

Elementfunktionen (und somit auch Getx und Gety) können auf alle Datenelemente der Klasse direkt zugreifen. Der Modifizierer const , bedeutet, dass diese Funktion keine Datenelemente der Klasse ändert. D.h. Schreibzugriffe auf Datenelemente werden innerhalb dieser Funktion verweigert.

 

  • Private und öffentliche Klassenelemente

Datenelemente lassen sich vor der Öffentlichkeit verbergen, indem sie mit private gekennzeichnet werden. Da die XY-Klasse Elementfunktionen erhalten hat, mit denen sich die Koordinaten abfragen lassen, brauchen wir keinen direkten Zugriff auf die Datenelementen mehr. Die neue Klassendeklaration erklärt folglich alle Datenelemente zum Privateigentum:

class XY{

private:
    double m_dX, double m_dY;
public:
    XY( );                                  
    XY(double dX, double dY);
    double Getx( ) const {return m_dX;} 
    double Gety( ) const {return m_dY;} 

};

Damit kapselt die Klasse ihre Datenelemente von der Außenwelt ab. Somit akzeptiert der Compiler außerhalb der Klasse keine Zugriffe wie den folgenden:

bottomRight.m_dX = 1.0;  

Diese Kapslung ist um so wichtiger, je komplizierter die Klassen werden, da Objekte nur auf genau definierten Wegen ihre Zustände ändern können.

 

  • Globale Funktionen

Wenn man eine neue Funktion benötigt, die etwas mit einer Klasse zu tun hat, aber die Klassendeklaration nicht ändern oder keine neue Klasse ableiten möchte, bietet sich eine globale Funktion an:

void show(XY xy)
{

print ("x=%f, y=%f \n", xy.Getx( ), xy.Gety( ));

}

Das funktioniert wenn die vorhandenen Elementfunktionen die erforderlichen Informationen liefern. Alternativ wäre eine friend-Funktion möglich, das macht jedoch eine Änderung der Klassendeklaration erforderlich.

 

  • Vererbung und Polymorphie

Ein Beispiel: Bei dieser kleinen Übung im objektorientierten Programmentwurf werden Objekte aus der realen Welt (hier das Sonnensystem und Raumschiffe) durch C++-Objekte nachgebildet(:

  • Die Basisklasse Orbiter und virtuelle Funktionen

Im Sonnensystem-Beispiel definieren wir die Basisklasse Orbiter, sie enthält Dinge, die Planeten mit Raumschiffen gemein haben. Ein Obiter kennt die Keplerschen Gesetze und weiß daher, wie man sich im Schwerefeld der Sonne zu bewegen hat.

Ein erster Entwurf der Orbiter-Klasse könnte so aussehen:

class Orbiter {

private:                //Datenelemente
    double m_dMass;
    XY         m_xyCurrent, m_xyPrior, m_xyThrust;
public:                 //Elementfunktionen
    Orbiter(XY xyCurrent, XY xyPrior, double dMass );    //Konstruktor                                  
    XY GetPosition( ) const;
    void Fly( );

};

Es müssen noch vor der Ableitung der Planeten- und Raumschiffklasse einige Dinge geändert werden. Das C++-Schlüsselwort protected erlaubt den abgeleiteten Klassen den Zugriff auf die Adtenelemente ihrer Basisklasse. Dann benötigen wir noch eine Elementfunktion, die den Orbiter anzeigt. Diese Display-Funktion sieht für Planeten natürlich anders aus als für Raumschiffe (selbst die Bork bauen Raumschiffe, die anders als Planeten aussehen hihi, aber Scherz beiseite Seven of Nine) Wer das jetzt nicht verstanden hat, braucht sich keine  Sorgen zu machen. Aber zurück zur Display-Funktion. Die Display-Funktion der Planeten-Klasse unterscheidet sich also von der Raumschiff-Klasse, sie muss daher "virtuell" sein und damit überschreibar. Das C++-Schlüsselwort virtual in der Deklaration der Basisklasse kennzeichnet Display als eine Funktion dieser Kategorie.

Nach den angesprochenen Änderungen sieht unsere Klassendeklaration so aus:

class Orbiter {

protected:               
    double m_dMass;
    XY         m_xyCurrent, m_xyPrior, m_xyThrust;
public:                
    Orbiter(XY xyCurrent, XY xyPrior, double dMass ) {
        m_xyCurrent = xyCurrent;
        m_xyPrior = xyPrior;
        m_dMass = dMass;
    }                                  
    XY GetPosition( ) const;
    void Fly( );
    virtual void Display( ) const;

};

Die virtuelle Display-Funktion zeigt wie Polymorphie (Vielgestaltigkeit) in C++ verwirklicht wird.

Das folgende Beispiel zeigt ein Orbiter-Feld mit Zeigern auf verschiedene Objekte, deren Klasse alle von Orbiter abgeleitet sind:

extern Orbiter* orbiterArray[ ];
for (int i=0;i<MAX; i++) {
orbiterArray[i]->Fly( );
orbiterArray[i]->Display( );

In diesem Beispiel stehen nicht die Objekte selbst im Datenfeld, sondern nur Zeiger auf die Objekte.

  • Reine virtuelle Funktionen

Im obigen Beispiel kann ein Programm Objekte der Klasse Orbiter anlegen. Das macht nur keinen sinn, da Orbiter ein abstraktes Konzept ist. Möchten wir verhindern, dass Objekte der Basisklasse Orbiter angelegt werden, deklarieren wir eine oder mehrere Elementfunktionen dieser Klasse als "rein virtuell":

virtual void Display( ) const=0;    //"=0" bedeutet  "rein virtuell"

Nun ist Orbiter eine "abstrakte Basisklasse" und der Compiler zwingt alle abgeleiteten Klassen, die Objekte erzeugen, zur Implementierung der Funktion Display.

  • Abgeleitete Klassen

Wir entwickeln zwei Klassen, die von Orbiter abgeleitet werden: Planet und Spaceship. Zunächst die simple Klasse Planet:

class Planet : public Orbiter {
public:                
    Planet (XY xyCurrent, XY xyPrior, double dMass )  :
             Orbiter (xyCurrent , xyPrior , dMass) { }  
    void Display( ) const;

};

Die erste Zeile sagt aus, dass die Klasse Planet öffentlich von der Klasse Orbiter abgeleitet  wird. Alle abgeleiteten Klassen erben die Datenelemente und Elementfunktionen ihrer Basisklasse mit Ausnahme der Konstruktoren und des Destruktors. In einer öffentlich abgeleiteten Klasse sind auch die geerbten öffentlichen der Basisklasse öffentlich und die geerbten geschützten Elemente der Basisklasse bleiben geschützt. Der Doppelpunkt in der Deklaration des Konstruktors Planet (...dMass) : ) bedeutet, dass der Basisklassenkonstruktor Orbiter zuerst aufgerufen wird und die Orbiter-Komponenten des Planet-Objekts anlegen kann.

Die Spaceship-Klasse ist komplizierter:

class Spaceship : public Orbiter {
private:
   double m_Fuel;
   XY m_xyOrientation;
public:                
    Spaceship (XY xyCurrent, XY xyPrior, XY xyThrust, double dMass,
                            double dFuel, XY xyOrientation )  :
             Orbiter (xyCurrent , xyPrior , dMass) {
                    m_dFuel =                    dFuel;
                    m_xyOrientation =    xyOrientation;
                    m_xyThrust =             xyThrust;          //m_xyThrust  ist ein Orbiterelement
}  
   void Display( ) const;
  void FireThrusters();

};

Sie müssen noch für den erforderliche Konstruktorcode sorgen, der die spezifischen Datenelemente initialisiert.

Eingebettete Objekte, Objekterzeugung auf der Halde, Globale Objekte

 

  • Kopierkonstruktoren

Der Kopierkonstruktor wird häufig vom Compiler generiert. Die Aufgabe des Kopierkonstruktors ist es ein neues Objekt derselben Klasse anzufertigen. Ein Inline-Kopierkonstruktor der Klasse XY sieht so aus:

 

XY (const XY& xy)   {
        m_dX = xy.m_dX;               
        m_dY = xy.m_dY;

}

Wenn der Compiler einen Kopierkonstruktor generiert, ruft diese Funktion den Kopierkonstruktor der Basisklasse auf. Wenn man den Kopierkonstruktor selbst schreibt, kann man auch entscheiden, welcher Basisklassenkonstruktor aufzurufen ist. Die Schreibweise const XY& ist eine Referenz. Mehr dazu weiter unten.

  • Zuweisungsoperatoren

Der Zuweisungsoperator ähnelt dem Kopierkonstruktor. Er legt aber kein neues Objekt an, sondern wirkt auf ein schon existierendes Objekt. Der Compiler generiert Standard-Zuweisungsoperatoren. Der Zuweisungsoperator ist ein Beispiel für einen überladenen C++-Operator. Wird ein eigener Inline-Zuweisungsopertator für die XY-Klasse geschrieben, dürfte er so aussehen:

 

const XY& operator= (const XY& xy)   {    // benutzt Referenzen
        m_dX = xy.m_dX;                                     // kopiert die Werte
        m_dY = xy.m_dY;
        return *this;

}

Die zurückgegebene XY-Referenz erlaubt die Verkettung der Zuweisungsoperatoren, wie z.B.:

xy1 = xy2 = XY(0.0, 0.0);

Der folgende Code zeigt ein Anwendungsgebiet für einen Zuweisungsoperator:

XY alpha(0.0, 2.0);

XY beta(1.0, 3.0);

beta = alpha;

 

  • Referenzparameter: const und nicht-const

Referenzparameter sind verkleidete Zeigerparameter. Er wird aus einem der zwei folgenden Gründe eingesetzt: Die Funktion ändert mit Hilfe diese Parameters einen Variable des aufrufenden Programms oder ein (größeres) Objekt soll nicht zur Übergabe an die Funktion auf den Stapel kopiert werden. Im ersten Fall ist die Referenz nicht konstant, im zweiten schon.

  • C++-Referenzen bei der Arbeit

Der folgende Code bildet die Erde nach:

XY xyCurrent(100.0, 200.0);                              //Erzeugt xy-Koordinatenpaar "current"
XY xyPrior(100.1, 200.1);                                  //Erzeugt xy-Koordinaten "prior"
Planet earth(xyCurrent, xyPrior, 1.0E+10); //Erzeugt die Erde

Wir setzen nun folgende Klassendeklarationen ein, die mittlerweile ein Stück gewachsen ist:


class XY{

public:
    double m_dX, double m_dY;
    XY( ) { m_dX = 0.0; m_dY = 0.0; }   //Standardkonstruktor                               
    XY(double dXarg, double dYarg)   //expilzieter Konstruktor
            { m_dX=dXarg; m_dY=dYarg;}
    XY(const XY& xy) { 
           m_dX = xy.m_dX; 
           m_dY = xy.m_dY; 
    }
const XY& operator=(const XY& xy) {       //Kopierkonstruktor
   m_dX = xy.m_dX;
   m_dY = xy.m_dY;
   return *this;

};

class Orbiter {
protected:
   double m_dMass;
   XY m_xyCurrent, m_xyProir, m_xyThrust;
public:                
    Orbiter (XY xyCurrent, XY xyPrior, double dMass) {
                            m_xyCurrent  =  xy_Current;
                             m_xyProir   =  xyPrior;
                             m_dMass =  dMass;
                }     
                XY GetPosition( );
                void Fly( );
          virtual void Display( ) = 0;

};

class Planet : public Orbiter {
public:                
    Planet (XY xyCurrent, XY xyPrior, double dMass )  :
             Orbiter (xyCurrent , xyPrior , dMass) { }  
    void Display( );

};

Um mit diesen Deklarationen ein Objekt namens Earth zu erzeugen, werden die XY-Konstruktoren in dieser Reihenfolge aufgerufen:

1. Der expilizite XY-Konstruktor erzeugt die Objekte xyCurrent und xyProir auf dem Stapel.

2. Der XY-Kopierkonstruktor kopiert die Objekte xyCurrent und xyPrior in die Argumentenliste des Planet-Konstrukors.

3. Der XY-Kopierkonstruktor kopiert die Objekte xyCurrent und xyPrior von der Argumentenliste des Planet-Konstruktors in die Argumentenliste des Orbiter-Konstruktors.

4. Der XY-Standardkonstruktor erzeugt die Elemente m_xyCurrent und m_xyProir und initialisieren sie mit (0,0).

5. Der XY-Zuweisungsoperator kopiert die Objekte xyCurrent und xyPrior von der Argumentenliste des Orbiter-Konstrukors in die entsprechenden Datenelemente.

 

Aus Effizienzgründen werden wir den Orbiter- und Planet-Code umbauen:

class Orbiter {
protected:
   double m_dMass;
   XY m_xyCurrent, m_xyProir, m_xyThrust;
public:                
    Orbiter (const XY& xyCurrent, const XY& xyPrior, double dMass)
       : m_xyCurrent(xyCurrent), m_xyPrior(xyPrior), m_dMass(dMass){ }    
              const   XY& GetPosition( );
                void Fly( );
          virtual void Display( ) = 0;
};

class Planet : public Orbiter {
public:                 //ohne Kopierkonstruktor und Zuweisungsoperator
    Planet (const XY& xyCurrent, const XY& xyPrior, double dMass)
       :  Orbiter (xyCurrent), m_xyPrior), m_dMass(dMass){ }    
  void Display( ) ;
};

Jetzt setzen die Orbiter- und Planet-Konstruktoren XY-Referenzen ein.

 

  • Rückgabe von Referenzen

Eine Funktion kann eine Referenz an ihren Aufrufer zurückgeben, was der Rückgabe eines Zeigers entspricht, und diese Referenzen können konstant sein oder auch nicht.  Die folgende neue Version von Getx liefert eine const-Referenz auf ein XY-Objekt:

const double& XY::GetConstRefx( ) const { return m_dX; }  

Die Funktion GetConstRefx kann auf der rechten Seite einer Zuweisung benutzt werden, aber weil die Referenz const ist, nicht auf der linken Seite. Daher führt die Anweisung.

topLeft.GetConstRefx( ) = 3.0;    //topLeft ist ein Objekt der Klasse XY

zu einem Kompilerfehler. Würden Sie das Schlüsselwort const aus der Referenz entfernen, wäre diese Zeile zulässig, und Sie könnten die Funktion zur Änderung der Werte im topLeft-Objekt benutzen .

 

  • Referenzen und Zeiger

Wenn der Compiler für eine Referenz praktisch so etwas wie "Zeigercode" generiert, stellt sich die Frage, wann eine Referenz besser ist und wann ein Zeiger. Diese beiden Elemente sind austauschbar. Man kann den Typ einer Referenz zum Beispiel genauso konvertieren wie den Typ eines Zeigers. Es gibt aber einen wichtigen Unterschied: ein Zeiger kann den Wert null haben. Wenn die eigene Funktion einen Zeigerparameter deklariert, kann der Aufrufer eine NULL als Zeiger angeben, und die Funktion kann den tatsächlichern Wert auf NULL testen. Handelt es sich dagegen um einen Referenzparameter, muß der Aufrufer ein Objekt angeben. Liefert die Funktion als Ergebnis einen Zeiger, kann Sie ebenfalls NULL abliefern, und der Aufrufer kann dies überprüfen. Gibt die Funktion eine Referenz an den Aufrufer zurück, muß die Funktion ein Objekt übergeben.

 

  • Objekterzeugung auf der Halde

In den bisher gezeigten Beispielen wurden alle Objekte auf dem Stapel angelegt und mit Ausnahme der virtuellen Funktion Display auch im direkten Zugriff benutzt. Objekte, die auf dem Stapel liegen, werden beim Verlassen des Gültigkeitsbereichs aufgelöst. Die Objekte lassen sich in C++ auch auf der Halde anlegen, wo sie so lange verfügbar sind, bis sie explizit wieder aufgelöst werden. Wo die Objekte auf der Halde liegen, merkt sich das Programm in Zeigern.

 

  • Die C++-Operatoren new und delete

Die Operatoren new und delete lassen sich mit den C-Funktionen malloc und free vergleichen. Auf folgender Weise läßt sich mit new ein Speicherblock anfordern:

char* pCommBuffer = new char[4096];

Allerdings werden öfters mit new neue Objekte auf der Halde angelegt:

Planet* pEarth = new Planet(XY(100.0, 200.0), XY(100.1, 200.1), 1.0E+10);

Anschließend enthält die Variable pEarth die Adresse eines Objekts der Klasse Planet und ist somit ein Zeiger. Beachte, dass sich die Konstruktorsyntax für die Objekterzeugung auf der Halde von der Syntax unterscheidet, mit der Objekte auf dem Stapel angelegt werden.

Um den Speicheblock wieder freizugeben, benutzt man den Operator delete, und zwar so:

delete [] pCommBuffer;

Auch die Erde wird man ganz einfach wieder los:

delete pEarth;

Die eingebetteten Objekte werden ebenfalls aufgelöst, wie es auch bei der Stapel-Version des Planet-Objekts geschieht, wenn das Objekt den Gültigkeitsbereich verläßt.

 

  • Objektzugriff über Zeiger

Sobald ein gültiger Zeiger auf ein Objekt vorhanden ist, lassen sich Elementfunktionen so aufrufen:

pErth->Fly();

Man braucht Zeiger, wenn Objekte polymorphisch angesprochen werden soll. Ein Beispiel:

Orbiter* pAny = new Planet(XY(100.0, 200.0),XY(100.1, 200.1), 1.0E+10);

C++ erlaubt diese Typkonvertierung, weol die Klasse Planet von Orbiter abgeleitet wird. Anschließend läßt sich die virtuelle Orbiter-Funktion aufrufen:

pAny->Display();

Zur Laufzeit wird die Display-Funktion der Planetenklasse aufgerufen, weil Display in der Orbiter-Klasse als virtuelle Funktion deklariert wird.

 

  • Virtuelle Destruktoren

Es ist zu beachten, dass Destruktoren nicht vererbt werden. Der   Compiler generiert für jede Klasse , die keinen Destruktor hat, einen Standard-Destruktor. Der Destruktor einer abgeleiteten Klasse ruft immer den Destruktor der Basisklasse auf. Was soll geschehen, wenn man einen Zeiger auf ein Objekt einer von Orbiter abgeleiteten Klasse hat und das Objekt auflösen möchte, aber die genaue Klassenzugehörigkeit nicht kennt? Der Aufruf des Orbiter-Destruktors würde nur zur Auflösung all derjenigen Bestandteile  führen, die schon von der Klasse   Orbiter festgelegt werden. Nehmen wir an, wir hätten auf der Halde ein Spaceship-Objekt angelegt, dessen Adresse einem Orbiter-Zeiger zugewiesen und anschließend auf folgende Weise verschwinden lassen:

Orbiter* pAny = new Spaceship(xyCurrent, xyPrior, xyTrust, dMass, dFuel, xyOrientation);

delete pAny;

Dabei wird zwar die richtige Speichermenge an die Speicherverwaltung zurückgegeben, aber die Auflösung des Spaceship-Objekts bleibt trotzdem unvollständig. Insbesondere wird der Destruktor des XY-Objekts m_xyOrientation nicht aufgerufen. Und wie lößt man das Problem? Man deklariert einen virtuellen Destruktor für die Orbiter-Klasse, was übrigens einen weiteren Eintrag in den vtables der Klasse Spaceship und Planet nach sich zieht:

virtual ~Orbiter( ) { }

Dabei braucht man in den abgeleiteten Klassen keine zusätzlichen Deklarationen und auch die Destruktoren nicht selbst zu implementieren, solange die Standardversionen ausreichen, die der Compiler generiert. Wenn man das obige Beispiel jetzt wiederholt führt die Anweisung

delete pAny;

zum Aufruf des Destruktors der abgeleiteten Klasse, in diesem Fall also der Klasse Spaceship. Er löst zuerst alle raumschiffspezifischen Dinge auf, einschließlich m_xyOrientation, und ruft dann den Orbiter-Destruktor auf.

 

  • Globale Objekte

In den Beispielen ging es bisher um Objekte, die auf dem Stapel oder auf der Halde liegen, und um solche, die in andere Objekte eingebettet werden. Globale Objekte werden noch vor dem Aufruf des main-Programms erzeugt und erst bei der Terminierung des Programms aufgelöst. Wie globale Variablen sind auch globale Objekte von allen Funktionen des Programms aus zugänglich.

 

Zeiger als Datenelemente, Einsatz des this-Zeigers, Referenzen auf Zeiger


Die Planet-Klasse arbeitet mit mehreren eingebetteten XY-Objekten. Da XY-Objekte nur 16 Byte lang sind und nicht von mehreren Planet-Objekten gleichzeitig benutzt werden, ist es sinnvoll, sie in die Planet-Klasse einzubetten. Diese Lösung bietet den Vorteil, daß die XY-Objekte bei der Auflösung des Planet-Objekts automatisch mit aufgelöst werden. Allerdings muss der Compiler die Deklaration der XY-Klasse noch vor der Planet-Klassendeklaration lesen.

Wie lassen sich nun Beziehungen zwischen zwei Objekten umsetzen? Nehmen wir an das Universum enthalte nicht nur Planeten und Raumschiffe, sondern auch Monde, und jeder Mond gehöre zu seinem Planeten. Unser Mond beeinflußt die die Bewegung der Erde um die Sonne nur in geringem Maße, aber die Erde beeinflußt die Bewegung des Mondes sehr stark. Die Erde hat nur einen Mond, aber andere Planeten haben mehrere Monde. Es ist also sinnvoll, in der Mondklasse einen Zeiger auf einen Planeten unterzubringen, wie in der folgenden Deklaration:

class Planet;

class Moon : public Orbiter {

private:

    Planet* m_pPlanet; //Zeiger auf dazugehöriges Planetenobjekt

public:

    Moon(XY& xyCurrent, XY& xyPrior, double dMass, Planet* pPlanet)

      : Orbiter(xyCurrent, xyPrior, dMass), m_pPlanet) {}

      void Display() const {}

Planet* GetPlanet() const {return m_pPlanet;}

void SetPlanet(const Planet* pPlanet) {m_pPlanet = pPlanet;}

};

Der Mondkonstruktor erwartet einen Planetenzeiger als Argument. Die Elementfunktion GetPlanet und SetPlanet ermöglichen den Zugriff auf den Zeiger. Zu beachten ist, dass der Compiler gar nicht die vollständige Deklaration der Klasse Planet zu kennen braucht, um die Deklaration von Moon zu bearbeiten. Die Vorwärtsdeklaration

calss Planet;

reicht aus , weil der Compiler nur den Platz für einen Zeiger zu reservieren braucht und alle Objektzeiger gleich groß sind. Die tatsächliche Größe des Moon-Objekts spielt keine Rolle. Zur Konstruktion eines Moon-Objekts braucht der Konstruktor einen Planet-Zeiger:

Planet* pEarth = new Planet(XY(100.00, 200.00), XY(100.11, 200.11), 1.0E+10);

Moon* pMoon = new Moont(XY(100.00, 200.00), XY(100.11, 200.11), 1.0E+10,pEarth);

Allerdings sollte man beim Löschen von solchen zusammenhängenden Objekten nicht die Übersicht verlieren. Wenn man in diesem Beispiel die Erde auflöst und das Programm weiterhin mit dem von ihr abhängigen Mond arbeitet, wird es abstürzen.

Bitte beachten:

Die Ergänzung des Sonnensystems um Monde macht die Funktion Orbiter::Fly wesentlich komplizierter. Der Zustand eines Orbiter-Objekts ist nicht länger nur von den sonnenbezogenen Koordinaten current und prior abhängig. Wir müssen aus Fly eine virtuelle Funktion machen, mit einer speziellen Version für Monde, oder wir müssen die Funktion Fly so ändern, dass sie die gegenseitige Beeinflussung der Himmelskörper berücksichtigt.

 

  • Einsatz des this-Zeigers

In der Sprache  C++gibt es eine spezielle Syntax, mit deren Hilfe das Programm einen Zeiger auf das aktielle Objekt ermitteln kann. Dieser Zeiger trägt den speziellen Namen this. Er kann als Funktionsargument benutzt werden oder einer Elementfunktion oder einem überladenen Operator als Rückgabewert dienen. Im folgenden Beispiel verknüpft eine Elementfunktion der Klasse Planet einen Mond mit seinem Planeten,

void Planet::ConnectToMoon(Moon* pMoon) {

PMoon->SetPlanet(this); // "dieses" Planetenobjekt

 

  • Referenzen auf Zeiger

SetPlanet und GetPlanet kann man zu einer Funktion zusammenfassen:

Planet*& Moon::GetPlanet() { return m_pPlanet; }

Die zurückgegebene Referenz auf einen Zeiger ist im Endeffekt ein Zeiger auf einen Zeiger. Daher kann man GetPlanet auf beiden Seiten einer Zuweisung einsetzen:

XY   xy11, xy12, xy21, xy22, xy31, xy32;

double dMass1, dMass2, dMass3;

Planet* pEarth = new Planet(xy11, xy12, dMass1);

Planet* pMars = new Planet(xy21, xy22, dMass2);

Moon* pMoon = new Moon(xy31, xy32, dMass3, pEarth);

assert(pMoon->GetPlanet()==pEarth);

pMoon->GetPlanet() = pMars;

Die eher C-gemäße Form ist:

Planet** Moon:: GetPlanet() {return &m-pPlanet;}

*(pMoon->GetPlanet()) = pMars;

In der MFC-Bibliothek gibt es zu einer Deklaration der Art

Planet*& Moon::GetPlanet();

häufig noch eine zweite Version:

Planet* Moon::GetPlanet() const;

Diese überladene Version ermöglicht den Einsatz von Get Planet (auf der rechten Seite von Zuweisungen) mit const-Zeigern auf Planet-Objekten. Wie das aussehen kann, zeigt das folgende Beispiel:

const Moon* cpMoon = new Moon(xy1, dMass, pEarth);

Planet* pPlanet = cpMoon->GetPlanet();

Die zweite Zeile wird der Compiler nicht annehmen, solange die zweite GetPlanet-Deklaration fehlt.

friend-Klassen und friend-Funktionen

  • friend-Klassen

Einige Klassen sind so eng miteinander verknüpft, dass man sie unter C++ zu  friend-Klassen erklären kann.

Es wäre praktisch wenn Moon-Objekte direkt auf die Datenelemente des Planet-Objektes zugreifen könnten, insbesondere auf die Planetenmasse und seine aktuelle Position. Das ist nur möglich, wenn die Planet-Klasse die Klasse Moon als Freund angibt:

class Planet : public Orbiter {

    friend class Moon; // keine vorherige Deklaration erforderlich

public:

    //Konstruktoren usw.

};

  • Globale friend-Funktionen

Nehmen wir an, in irgendeiner Anwendung repräsentieren die Klassen XY einen Vektor und Sie brauchen den Tangens des Winkels. Nun kann man einen Funktion tan schreiben:

double XY::tan() {

    if (m_dY !=0.0) {

    return m_dX / m_dY;

    }

    else {

    return 0.0;

    }

}

Anschließend wird tan wie jede Elementfunktion aufgerufen:

XY    vector(1.0, 1.0);

double result = vector.tan();

Man kann auch eine vertrautere Aufrufsyntax bevorzugen, indem man tan als globale Friendfunktion der Klasse XY deklariert:

class XY {

    friend double tan(const XY& xy);

private:

    double m_dX, m_dY;

public:

//Konstruktoren  usw.

};

double tan(const XY& xy) {

    if (xy.mdY != 0.0){

        return xy.m_dX/xy.m_dY;

    }

    else {

        return 0.0;

    }

}

Nun ruft man die tan-Funktion folgendermaßen auf:

XY       vector(1.0, 1.0);

double result = tan(vector);

Es kommt nicht zu einem Konflikt mit den tan-Funktionen der Standardbibliothek, weil der Compiler anhand der Parameter die richtige Funktion auswählt.

       

Statische  Klassenelemente

Wenn man die Anzahl aller aktiven Orbiter braucht, kann man eine globale Variable definieren. Das würde aber gegen die Kapslung verstoßen. C++ bietet statische Datenelemente und statische Elementfunktionen an, die aber nicht mit einem bestimmten Objekt verknüpft sind, sondern mit der Klasse.

  • Statische Datenelemente

Mit der statischen int-Variablen s_nCount sieht die Deklaration der Orbiter-Klasse so aus:

class Orbiter {

protected:

    double m_dMass;

    XY    m_xyCurrent, m_xyProir, m_xyThrust;

public:

    static int s_nCount;

    Orbiter(XY& xyCurrent, XY& xyProir, double dMass);

    const XY& GetPosition() const;

    void Fly();

    virtual void Display() const = 0;

}:

Es gibt immer nur eine Kopie von s_nCount, und zwar unabhängig von der Zahl der Orbiter-Objekte. Die Klassendeklaration deklariert die Klasse, aber sie reserviert keinen Speicher für s_nCount. Es ist zusätzlich eine globale Definition der Variablen erforderlich:

int Orbiter::s_nCount = 0; //Initialisierung ist optional (d.h. nicht unbedingt erforderlich)

Weil die Variable in der Klasse als public deklariert wird, läßt sich sie sich zum Beispiel auf folgende Weise verwenden:

Orbiter::s_nCounter++;

Wenn man innerhalb einer Orbiter-Elementfunktion mit s_nCount arbeitet, z.B. im Konstruktor, kann man das Orbiter:: weglassen. Sofern man ein konstantes statisches Datenelement mit

static const int s_nMaxCount;

deklariert, läßt sich die Variable trotzdem noch global initialisieren, z.B. so:

const int Orbiter::s_nMaxCount = 256;

 

  • Aufzählungstypen als Vereinfachung

Wenn eine Klasse einen konstanten statischen Integer braucht, kann man zu einer Abkürzung greifen, die eine zusätzliche Initialisierung überflüssig macht. Dazu nimmt man einfach eine enum-Anweisung in die Klassendeklaration auf:

enum {nMaxCount = 256};

Der Compiler baut diese Zahl direkt in den Programmcode ein, wenn man nMaxCount benutzt, wie er es auch für eine mit define definierte symbolische Konstante tun würde. Ein statisches Datenelement wie s_nCount muß dagegen vom Prozessor erst einmal aus dem Datenelement ausgelesen werden.

Statische Elementfunktionen

Wäre das statische Orbiter-Datenelement s_nCount privat, bräuchten Sie für den Zugriff eine statische Elementfunktion. Eine öffentliche Funktion Count, die eine Referenz auf einen Integer liefert, ließe sich auf beiden Seiten einer Zuweisung einsetzen:

public:

    static int& Count() { return s_nCount; }

Hier ein paar Beispiele mit der neuen Count-Funktion:

int nOldCount = Orbiter::Count()++;

int nNewCount = Orbiter::Count();

assert (nNewCount == nOldCount+1;);

Ein interessanter Einsatzbereich der statischen Elementfunktionen ist die Erzeugung von Objekten. Nehmen wir an, Sie bräuchten wieder ein Objekt im Orbit und wüßten aber erst zur Laufzeit, um welche von Orbiter abgeleitete Klasse es sich handelt.  Da hilft eine statische Konstruktionsfunktion weiter, die mit Hilfe einer switch-Anweisung das gewünschte Objekt liefern kann.

static Orbiter* Orbiter::MakeNew(int nSelection, XY& xyCurrent,XY& xyProir, double dMAss, XY& xyThrust, double dFuel, xyOrientation, Planet* pPlanet)

{

    switch (nSelection) {

    case 0:

        return new Planet(xyCurrent, xyProir, dMAss);

    case 1:

        return new Spaceship(xyCurrent, xyProir, dMAss, dFuel, xyOrientation);

    case 2:

        return new Moon(xyCurrent, xyPrior, dMAss, pPlanet);

    default:

        return NULL;

    }

}

Nun kann man sehr leicht ein ganzes Datenfeld mit Orbiter-Zeigern füllen:

Orbiter* pOrbiterArray[MAX]

XY    xyCurrent, xyPrior, xyThrust, xyOrientation;

double dMass, dFuel;

Planet* pPlanet;

for (int i=0; i<MAX; i++) {

    pOrbiterArray[i] = Orbiter::MakeNew(i % 3, xyCurrent, xyPrior, dMass, xyThrust, dFuel,  xyOrientation, pPlanet);

}

Nun hat man Planet-, Spaceship- und Moon-Zeiger als Orbiter-Zeiger im Datenfeld untergebracht. Wie findet man nun heraus, um welche Objektarten es sich handelt? Man braucht die genaue Klasse nicht zu kennen, solange man nur Fly oder Display aufruft und die Objekte dann alle auflöst. Das hilft aber nicht viel, falls man tatsächlich die genaue Klasse kennen muss. Man kann natürlich ein neues Datenelement in Orbiter aufnehmen, das zur Unterscheidung der Klasse dient, und eine entsprechende Zugriffsfunktion vorsehen.

                               

Überladene Operatoren

Anfangs wird man wohl kaum überladene Operatoren entwickeln, aber sehr wahrscheinlich mit den überladenen Operatoren der MFC-Bibliothek arbeiten.

Überladene Operatoren sind nützlich, weil man die Entwicklung einer C++-Anwendung vereinfachen kann und sie lesbarer machen. Aber manche Programmierer verlieren das rechte Augenmaß und schließen weit über das Ziel hinaus. Es ist ratsam, nur dann neue Operatoren zu schreiben, wenn ihre Bedeutung wirklich  leicht zu erkennen ist und dem "natürlichem Umgang" mit den Objekten entspricht. Hinter einem Operator verbirgt sich nicht anderes als der Aufruf einer speziellen Elementfunktion, und manchmal ist ein normler Funktionsaufruf sinnvoller.

 

  • Elementfunktionen als Operatoren

Viele Operatoren werden als Elementfunktion einer Klasse implementiert. In den Beispielen sind XY-Objekte normalerweise xy-Koordinatenpaare, und die Idee liegt nahe, Koordinatenpaare zu addieren und zu subtrahieren. Die entsprechenden Operatoren sehen so aus:

XY XY::operator +(const XY& xy) const { //addieren

return XY(m_dX + xy.m_dX, m_dY + xy.m_dY);

}

XY XY::operator -(const XY& xy) const { //subtrahieren

return XY(m_dX -  xy.m_dX, m_dY - xy.m_dY);
}

Zu beachten sind hier die Referenzen. Bei der Klasse XY mit ihren kleinen Objekten könnte man auch auf die Referenzen verzichten, damit aber auch auf die Möglichkeit mit Referenzen die Effizienz des Programms zu steigern. Außerdem werden sie von den Zuweisungsoperatoren gebraucht.

Probieren wir einmal den Additionsoperator aus:

XY xy1(1.0, 2.0), xy2(3.0, 4.0);

XY xy3 = xy1 + xy2; //sollte (4.0, 6.0) sein

Das unäre Minus ist ein weiterer nützlicher Operator. Er hat kein Argument.

XY XY::Operator -() const { //unäres Minus

    return XY(-m_dX, -m_dY);

}

Hier ist der Code für die Operatoren * und *=:

XY operator *(double dMult) { //skalare Multiplikation

    return XY(m_dX * dMult, m_dY * dMult);

}

const XY& operator *=(const double dMult) {

    m_dX *= dMult;

    m_dY *= dMult;

    return *this;

}

Und so wird er eingesetzt:

XY xy1(1.0. 2.0);

XY xy2 = xy1 * 3.0; // ergibt (3.0, 6.0)

xy2 *= 2.0;    // ergibt (6.0, 12.0)

  • Konvertierungsoperatoren

Sowohl C als auch C++ erlauben viele automatische Umwandlungen unter den Standardtypen. Nehmen wir als Beispiel folgende Zeilen:

int nRadians = 2;

double dResult = atans(nRadians);

Die Funktion atan erwartet als Argument einen double-Wert. Daher konvertiert der Compiler den Integer nRadians in eine double, den er an die Funktion übergibt.

Wie steht es um die Umwandlungen zwischen C++-Klassen? Man muss die entsprechenden Vorschriften selbst implementieren. Gehen wir von einer String-Klasse aus, die ein Zeichenfeld m_pch enthält. Die folgende Operatorfunktion liefert einen konstanten Zeiger auf das interne Datenfeld des Objekts:

String::operator  const char*() const

{

    return (const char*) m_pch;

}

Anschließend kann man überall dort ein String-Argument angeben, wo der Compiler ein const char* erwartet:

String s1("test"); // S1 aus Zeichenkette erzeugen

String s2;

char c1[20];

int  n= strlen(s1); //ok

strcpy(s2, s1); //kompiliert nicht, weil das erste Argument nicht const ist

strcpy(c1, s1); //ok

Es wurde absichtlich kein (nicht konstanter) Operator char* deklariert, weil die Daten nicht so leicht ins Objekt hineingehen, wie sie sich herausziehen lassen (man könnte versehentlich Feldgrenzen überschreiten).

In der MFC-Bibliothek gibt es eine recht nützliche Klasse namens CString mit demselben überladenen Konvertierungsoperator const char*. Man setzt diesen Operator recht oft ein, wenn man MFC-Programme schreibt.

  • Globale Operatoren

Die bisher vorgestellten Operatoren waren Elementfunktionenvon Klassen. Nehmen wir an, man möchte einen neuen Operator einsetzen, aber keine neue Klasse ableiten. Wenn es in der Klasse öffentliche Datenelemente und genug Zugriffsfunktionen für die erforderlichen Daten gibt, kann man auch einen globalen Operator für die Klasse schreiben.

Weil wir die Divisionsfunktion für die Klasse XY bisher nicht geschrieben haben, holen wir das jetzt nach. Aber der Code unterscheidet sich etwas vom überladenen Multiplikationsoperator, der bereits bekannt ist:

XY operator / (const XY& xy, const double dDiv) //skalare Division

{

    return XY(xy.m_dX / dDiv, yx.m_dY / dDiv);

}

Zu beachten ist, dass die Datenelemente m_dX und m_dY bei dieser Schreibweise öffentlich sein müssen. Sind sie es nicht, braucht man die entsprechenden XY-Zugriffsfunktionen. Globale Operatoren können die mathematischen Fähigkeiten einer Klasse erweitern. Der XY-Multiplikationsoperator kann in der Form, wie gezeigt, in folgender Anweisung eingesetzt werden:

XY xy2 = xy1 * 3.0;

Aber er funktioniert nicht in der folgenden Zeile:

XY xy2 = 3.0 * xy1;

In diesem zweiten Fall braucht man eine Hilfsfunktionen der folgenden Art:

XY operator *(const double dMult, const XY& xy) // skalare Multiplikation

{

    return XY(xy.m_dX * dMult, xy.m_dY * dMult);

}

   

Trennung von Klassendeklarationen und Code

In den bisher gezeigten Beispielen wurden Klassendeklarationen und Code vermischt. Die Modularität von C++-Programmen hängt allerdings von der sauberen Trennung zwischen Implementationscode und Klassendeklaration ab. Die "Anwender" der Klassen brauchen nur die Deklarationen. Die Klassen-"Autoren" entwickeln den Code und entschließen sich vielleicht dazu, nur die kompilierte und linkfähige Form auszuliefern.

Oft werden Klassendeklarationen in einer oder mehreren Kopfdateien zusammengefaßt, wie zum Beispiel auch in der MFC-Bibliothek, während der Code, aufgeteilt in viele kleine, unabhängig voneinander linkbare Module, zu einer Bibkiothek(LIB) zusammengefasst wird. Die Anwendungsprogrammierer nehmen dann die Kopfdateien in ihre C++-Quelltexte auf und linken das Programm mit der entsprechenden Bibliothek.

Die Kopfdateien für das Sonnensysetm könnte folgendermaßen aussehen (allerdings ohne Mond):

// Solar.h - Klassendeklaration

class XY { // alle Elementfunktionen sind  sin Inline

private:

    double m_dX, m_dY;

public:

    XY()

    XY(double dX, double dY);

    XY (const XY& xy);

    const XY& operator =(const XY& xy);

    XY operator +(const XY& xy) const;

    XY operator -(const XY& xy) const;

    XY operator *(const double dMult) const;

    XY operator /(const double dDiv) const;

   XY operator -() const;

};

class Orbiter {

protected:

    double m_dMass;

    XY m_xyCurrent; m_xyPrior, m_xyThrust;

public:

    static int s_nCount;

    Orbiter(XY& xyCurrent, XY& xyPrior, double dMass); //Inline

    const XY& GetPosition() const;

    void Fly();

    virtual void Display() const = 0;

};

class Planet : public Orbiter {

private:

    //Darstellung ohne Kopierkonstruktor und Zuweisungsoperator

    Planet(XY& xyCurrent, XY& xyPrior, double dMass) :

        Orbiter(xyCurrent, xyPrior, dMass) { }

    void Display();

};

class Spaceship : public Orbiter {

private:

    double m_dFuel;

    XY  m_xyOrientation;

   public:

    //Darstellung ohne Kopierkonstruktor und Zuweisungsoperator

    Spaceship(XY& xyCurrent, XY& xyPrior, XY& xyThrust, double dMass,

                     double dFuel,   XY& xyOrientation);

    voidDisplay();

    void FireThrusters();

};

//hier folgen die XY-Inline-Funktionen

 

Manche Programmierer ziehen es vor, die include-Anweisungen zu schachteln. Aber dann muss man eine versehentliche mehrfache Aufnahme der Kopfdateien verhindern, und zudem werden die Abhängigkeitem zwischen den Dateien komplizierter. Die MFC-Programme schachteln per Konvention keine Kopfdateien. Die CPP-Dateien der MFC-Bibliothek zeigen immer genau auf, welche Kopfdateien eingelesen werden.

Zu beachten ist, dass die Inline-Funktionen nach den Klassendeklarationen folgen. Im kleinen Sonnensystem ist das zwar nicht erforderlich, aber es verbessert die Lesbarkeit der Deklarationen und erlaubt die Verlegung der Inline-Funktionen in eine zusätzliche Kopfdatei. Einige Anwendungen machen solch eine Trennung erforderlich, weil der Inline-Code von vorherigen Deklarationen abhängig sein könnte.

Diejenigen Elementfunktionen, die nicht als Inline-Funktionen implementiert werden ,stehen in der CPP-Datei, in diesem Fall also Orbiter::Fly, Orbiter::GetPosition, Planet::Display, Spaceship::Display und Spaceship::FireThrusters. Es bleibt einem überlassen, wie man die Funktionen anordnet. Wenn sie alle in derselben Datei liegen, werden alle eingebunden, auch wenn tätsächlich nur eine benutzt wird (es sei denn, man schaltet die funktionsweise Bindung des Kompilers ein) Meistens werden die Elementfuktionen einer Klasse in einer eigenen Datei zusammengefasst. Diese Datei mit dem Klassencode ist auch der richtige Ort für die Definitionen der statischen Elementvariablen.


So hier endet der kleine C++-Crash-Kurs. "Seven of  Nine" geht jetzt hoffentlich zufrieden in ihren Allkoven (oder wie das Ding heißt). Wenn Du bis hierhin einigermassen durchgehalten hast, hast Du alle Chancen bald selbst faszinierende Spiele oder was auch immer (den einfachen Rest eben) zu programmieren. Auch wenn Dir dieser Kurs zu komplex erschien (weil vielleicht zu viel vorausgesetzt wurde), sollte Dich das nicht entmutigen. Aller Anfang ist bekanntlich schwer. Für C++-Kurse und jede Spezialisierung gibt es heute (und das war nicht immer so) viele gute Bücher, die sich im Internet oder in der Buchhandlung leicht finden lassen.


 
HTML Uhr
Heutiges Datum
Zurück zum Seiteninhalt | Zurück zum Hauptmenü