2. Abend

 

 

Ziele :

Ihr seid nach diesem Abend in der Lage dieses Beispiel nachzuvollziehen und zu erklären !

Ihr könnt new und delete verwenden um dynamisch Objekte zu erzeugen und zu löschen

Ihr könnt in einer Klasse mehrere Funktionen mit dem gleichen Namen aber unterscheidlichen Parametern definieren (Überladen von Funktionen)

Das Beispiel

 

Das Beispiel, das wir heute komplett ansehen werden, ist die Musterlösung zur Übung von letzter Woche. Wir werden zusammen eine CD Liste implementieren, wobei nicht die Funktionalität im Vordergrund steht, sondern die Schritte, wie man zu einer ersten Version der CD Liste zu kommen, auf der man die "Datenbank"-Funktionen implementieren kann. Betrachten wir zuerst die Klassen, die wir implementieren werden :

 

CD :

Die Klasse CD enthält folgende Angaben zu einer CD : Den Titel, den Interpreten, die Länge der CD. Zusätzlich ergänzen wir die Klasse mit einem Zeiger (!) auf die CD, die in der Liste folgt.
Als erstes erzeugen wir also ein neues, leeres Projekt.

Wir erzeugen danach eine Datei CD.h, die die Deklaration unserer Klasse enthält. Diese Datei müssen wir jedesmal einbinden, wenn wir auf die Klasse CD bezug nehmen :

 

CD.h

 

#ifndef CD_H    // wir verhindern damit
#define CD_H    // dass diese Header
                // Datei mehrmals eingebunden
                // wird

// Wir verwenden die Klasse string in dieser
// Datei, also müssen wir die passende Biblio.
// Datei einfügen
#include <string>

// Die Klasse string ist im Namespace std;
using namespace std;

class CD
{
    public:
        CD();   // Einfacher Konstruktor
        ~CD();  // Destruktor

        // Zum Setzen des Titels
        void SetTitle(const string& Title);
        // Zum Setzen des Interpreten
        void SetInterpret(const string& Interpret);
        // Zum Setzen der Laenge
        void SetLength(double length);
        // Zum Setzen des Zeigers auf die
        // naechste CD in der Liste
        void SetNext(CD* Next);
        // Hole Zeiger auf naechstes
        // Element in der Liste
        CD* GetNext();
        
    private:
        string m_Title;
        string m_Interpret;
        double m_Length;
        CD*    m_Next;
};

#endif
 
 

In dieser Klasse fehlt noch einiges, zum Beispiel können wir nichts ausgeben auf diese Weise, denn wir können nur die Eigenschaften der CD setzen aber nicht ausgeben. Nützlich wäre zum Beispiel ein Ausgabe-Funktion. Doch die folgt etwas später.

 

Die Implementation (das .cpp-File) enthält dann die Funktionen, die ziemlich einfach sind und darum hier nicht ganz abgedruckt sind. Als Ausnahme nur der Konstruktor, denn es ist wichtig, dass wir den Zeiger auf 0 initialisieren. Die komplette Datei CD.cpp kannst Du dir einfach downloaden.

 

 

#include "CD.h"

// Konstruktor
CD::CD()
{
    // Alle Datenelemente
    // initialisieren, die
    // keinen Konstruktor
    // haben.
    m_Length = 0.0;
    // Zeiger auf 0 setzen
    m_Next = 0;
}

 

 

CDList :

Diese Klasse versteckt (kapselt) das Anfügen eines CD-Elementes am Ende der Liste. Sie verwaltet den Zeiger auf das erste Element und das Letzte ser Liste, so dass wir das im Hauptprogramm nicht mehr müssen. Auch wird es dadurch einfacher, mehr als eine Liste zu halten.

CDList.h

 

#ifndef CDLIST_H    // Verhindern dass diese
#define CDLIST_H    // Datei mehrmals includiert
                    // wird, der sogenannte
                    // Include - Blocker

// Die Klasse CDList wird sicher
// eng mit der CD - Klasse arbeiten
// also müssen wir deren Header-Datei
// includieren
#include "CD.h"

class CDList
{
    public:
        CDList();   // Einfacher Konstruktor
        ~CDList();  // Destruktor

        // Mit dieser Funktion eine neue
        // CD hinten anfügen
        void AddNewCD(CD* newCD);

        // Zeiger auf die vorderste CD der
        // List erhalten
        CD* GetFirst();

        // Zeiger auf die hinterste CD der
        // Liste erhalten
        CD* GetLast();

    private:
        CD* m_First;
        CD* m_Last;
};

#endif
 
 

Mit dieser Header-Datei deklarieren wir also unsere einfache Listenklasse für CD's. Die wichtigsten zwei Funktionen betrachten wir genauer :

CDList.cpp

 

#include "CDList.h"

// Konstruktor
CDList::CDList()
{
    m_First = 0;
    m_Last = 0;
}

void CDList::AddNewCD(CD* newCD)
{
    if(m_First == 0 && m_Last == 0)
    {
        // diese Liste ist noch leer
        m_First = newCD;
        m_Last = m_First;
    }
    else
    {
        // diese Liste ist nicht leer.

        // m_Last zeigt auf das letzte
        // Element in der Liste. Diesem
        // wird das neue angehängt
        m_Last->SetNext(newCD);

        // Das neue Element ist nun das
        // letzte also m_Last korrigieren
        m_Last = newCD;
    }
}

 

 

Der Konstruktor ist zwar ziemlich klar und einfach. Wir werden aber im Unterricht sehen was geschieht, wenn wir die Zeiger nicht initialisieren (oder probier es selber).
In der Funktion "AddNewCD" überprüfen wir zuerst ob die Liste leer ist. Da die beiden Zeiger m_First und m_Last in Konstruktor auf 0 gesetzt wurden, können wir anhand ihnen feststellen ob es schon Elemente in der Liste gibt.
War die Liste noch Leer wird das neue Element "newCD" das erste indem m_First darauf zeigt. Und ist nur ein Element in der Liste ist der Zeiger auf das Letzte gleich dem Zeiger auf das erste.
War die Liste nicht leer kommen wir in den else Teil der Anweisung. Dort sorgen wir einfach dafür, dass das aktuell letzte Element seinen next-Zeiger auf dieses neue Element zeigen lässt. Danach setzen wir den m_Last Zeiger auf dieses neue letzte Element.

 

 

Test der Liste mit einem einfachen main

Wir sind soweit, dass wir nun eine main - Funktion schreiben können mit der wir die CDList-Klasse brauchen können und Elemente einfügen.

main.cpp

 

#include "CD.h"     // Die CD-Klasse
#include "CDList.h" // Die CDList-Klasse

#include <conio.h>  // für das getch
#include <crtdbg.h> // für die _CrtSetDbgFlag
                    // Funktion
#include <iostream> // für cin, cout
#include <string>   // für die strings

using namespace std;

int main()
{
    // Dieser Funktionsaufruf bewirkt, dass beim
    // Beenden des Programms Speicherlecks angezeigt
    // werden.
    _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);

    int weiter = 'j';   
    // Der Datentyp für die Variable "weiter" müsste
    // eigentlich char sein, aber die Funktion
    // getch liefert den ASCII-Wert der gedrückten
    // Taste als int zurück.

    // Ich verwende für alle CD's die gleichen Daten
    string ElvisName("Elvis");
    string ElvisTitle("Viva Las Vegas");
    double Laenge = 36.0;

    // Neue Liste auf dem Stack-Speicher anlegen
    CDList theList;

    while(weiter == 'j')
    {
        // neue CD auf dem Heap-Speicher anlegen
        CD* pCD = new CD;

        pCD->SetInterpret(ElvisName);
        pCD->SetTitle(ElvisTitle);
        pCD->SetLength(Laenge);

        // Einfach diese CD in die Liste einfügen
        theList.AddNewCD(pCD);

        cout << "Noch eine CD ? [j/n]" << endl;

        weiter = getch();
    }

    return 0;
}

 

Das Projekt besteht nun aus folgenden Dateien :

Dateien in unserem Projekt:

main.cpp

CD.h

CD.cpp

CDList.h

CDList.cpp

 

 

Nach erfolgreicher Kompilation können wir das Programm laufen lassen. Der aufmerksame Leser wird bemerkt haben, dass in unserem Programm einige new - Aufrufe stattfinden aber keine delete ! Natürlich ist das Absicht, damit wir auch die Wirkung von unserem Aufrug _CrtSetDbgFlag sehen können. Hier ein Beispiel wenn wir das Programm 4 CD's erstellen lassen.

 

 

 

Speicherlecks bereinigen

Die Speicherlecks entstehen dadurch, dass der Speicher den wir mit new allozieren nie mit delete freigegeben wird. Wir könnten kurz vor dem Programmende eine Schleife einbauen, die alle Element in der Liste holt, und den Speicher aufräumt :

 

 

    CD* actual = theList.GetFirst();

    while(actual != theList.GetLast())
    {
        CD* toDelete = actual;
        actual = toDelete->GetNext();
        delete toDelete;
    }
    delete actual;

Hier der Code für das ganze, geänderte main.cpp

 

 

Noch schöner im Sinne der Datenkapselung wäre es aber, wenn wir den Code der die Liste aufräumt auch gleich in der Liste haben. Die Liste soll sich doch selber aufräumen. Wo ? Am Ort der für das Aufräumen geschaffen wurde, dem Destruktor von CDList.

 

 

// Destruktor
CDList::~CDList()
{
    // hole das erste element
    CD* actual = m_First;

    // solange actual nicht 0 ist
    while(actual != 0)
    {
        // setze den m_First-Zeiger auf das zweite
        // Element. Das Letzte Element würde uns
        // hier den Zeiger 0 zurückgeben !
        m_First = actual->GetNext();
        // Lösche das Bisherige erste Element
        delete actual;

        actual = m_First;
    }
}

 

Erste Erweiterung

 

 

Funktionen überladen

"Funktionen überladen" bedeutet eine zweite (oder mehrere) Funktionen mit dem gleichen Namen wie eine bereits vorhandene zu schreiben. Diese Funktion unterscheidet sich von den Funktionen, die gleich heissen durch andere Parameter ! Im Grunde genommen kennen wir das bereits durch verschiedene Konstruktoren. Wenn wir einen Konstruktor mit Parameter haben ist das gelichwertig mit einer Funktion überschreiben.
In unserem Beispiel mit den CD's können wir in der Klasse CDList eine zweite Funktion AddNewCD schreiben. Sie soll aber keinen Parameter haben, sondern new selber aufrufen und diesen Zeiger als return - Wert haben. Es folgt nur ein kleiner Ausschnitt aud der Header Datei:

CDList.h

 

class CDList
{
    public:
        CDList();   // Einfacher Konstruktor
        ~CDList();  // Destruktor

        // Mit dieser Funktion eine neue
        // CD hinten anfügen
        void AddNewCD(CD* newCD);

        // Mit dieser Funktion eine neue
        // Cd erzeugen und auch gleich hinten
        // anfügen
        CD* AddNewCD();

Diese neue Funktion unterscheidet sich nur durch die Parameterliste und den Rückgabewert von der gleichnamigen Funktion. Die Implementation im .cpp File ist ziemlich einfach, das sie sogar die andere AddNewCD - Funktion aufruft.

CDList.cpp

 

CD* CDList::AddNewCD()
{
    CD* newCD = new CD;
    AddNewCD(newCD);
    return newCD;
}

 

 

Also heisst das Überladen von Funktionen nicht viel mehr als mehrere Funktionen mit gleichem Namen aber unterschiedlichen Parametern zu haben. Das Überladen von Funktionen funktioniert auch mit Funktionen, die nicht zu Klassen gehören.

 

 

Funktionen überladen, 2. Beispiel

Um dieses Konzep besser zu verstehen betrachten wir ein zweites Beispiel mit unserer geliebten Auto-Klasse. Nehmen wir an unser Auto könnte mit in eine beliebige Richtung in der Ebene beschleunigen. Das heisst die Beschleunigung hat eine x und eine y - Komponente.

Auto.h

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Auto.cpp

 

#ifndef AUTO_H  // Include-Blocker
#define AUTO_H

class Auto
{
    public:
        Auto();     // Konstruktor
        ~Auto();    // Destruktor

        void Beschleunigen(double xa, double xy);

        void Beschleunigen(double xa);

    private:
        // Beschleunigung in x Richtung
        double m_xa;
        // Beschleunigung in y Richtung
        double m_ya;
};

#endif

Und hier gleich die .cpp - Datei:

#include "Auto.h"

Auto::Auto()
{
    m_xa = 0.0;
    m_ya = 0.0;
}

Auto::~Auto()
{
}

void Auto::Beschleunigen(double xa)
{
    m_xa = xa;
    m_ya = 0.0;
}

void Auto::Beschleunigen(double xa, double ya)
{
    m_xa = xa;
    m_ya = ya;
}

 

 

Beim Aufruf haben wir nun die Wahl, welche Version der Funktion aufgerufen wird.

 

 

    Auto einAuto;

    // Aufruf der Funktion
    // mit nur einem Parameter
    einAuto.Beschleunigen(0.5);

    // oder mit zwei Parametern
    einAuto.Beschleunigen(2.0, 1.2);

 

Operatoren Überladen

 

 

Nun folgt etwas besonderes, nicht ganz einfaches, aber etwas entscheidendes zur C++ Sprache. Das Überladen von Operatoren ermöglicht es selber definierte Datentypen (Klassen) so zu verwenden wie eingebaute. Nehmen wir an wir haben eine selber definierte Klasse Complex.

 

 

class Complex
{
    public:
        // Konstruktor
        Complex();
        // Destruktor
        ~Complex();

    private:
        double m_real;
        double m_imag;
};

Natürlich mit noch mehr Elementfunktionen, um die Datenelemente zu setzen und zu lesen, etc.

Falls wir komplexe Zahlen (Objekte der Klasse Complex) genauso behandeln könnten wie einen eingebauten Datentypen wie zum Beispiel double oder int, könnten wir folgenden Code schreiben:

// Konstruktor mit Parameter
Complex Zahl1(1.0, 3.0);

Complex Zahl2(2.8, 3.5);

Complex Zahl3 = Zahl1 + Zahl2;

Um solchen Code zuzulassen müssen wir dem Compiler nur zeigen wie er die Plus-Operation ausführen muss. Wir überladen den operator + ! Das sieht dann so aus :

Complex.h

 

class Complex
{
    public:
        // Konstruktor
        Complex();
        // Destruktor
        ~Complex();

        Complex operator+(const Complex& c1);

Und der Code in der .cpp-Datei

Complex.cpp

 

Complex Complex::operator+(const Complex& c1)
{
    Complex c2;
    c2.m_real = m_real + c1.m_real;
    c2.m_imag = m_imag + c1.m_imag;

    return c2;
}

Wir brauchen hierfür also das Schlüsselwort operator. Was der Code dann in der .cpp-Datei wirklich macht, ist uns selber überlassen. Wir könnten zum Beispiel einen operator+ schreiben, der statdessen ein Subtraktion ausführt, was aber nicht den Erwartungen entsprechen würde. Wir könnte aber andere Sinnvolle Tätigkeiten ausführen, wie zum Beispiel in eine Datei zu schreiben, dass der Operator aufgerufen wurde und mit welchen Parametern.

 

Es gibt noch viele Operatoren, die wir überschreiben können :

+ (add)

- (sub)

* (multi.)

== (Vergleich)

= (Zuweisung)

! (Negation)

<< (Insertion)

>> (Extraction)

etc.  

Eine komplette Liste findet ihr in euren Unterlagen (Kapitel 8. Operator Overloading). Wichtig sind zum Beispiel der Zuweisungsoperator, der aufgerufen wird, wenn wir einem Objekt unserer Klasse ein anderes Objekt unserer Klasse zuweisen wollen.

 

 

Complex c1;
Complex c2;
c2 = c1;
// Zuweisungsoperator wird aufgerufen

Da wir in unserer Complex-Klasse keinen Zuweisungsoperator definiert haben, produziert der Compiler einen eigenen, der einfach alle unsere Datenelement vom Objekt c1 in das Objekt c2 kopiert. Das geht bei unserer Klasse Complex problemlos, es gibt aber Situationen in der wir genau bestimmen müssen wie ein Objekt dem anderen zugewiesen wird.

Interessant für uns ist auch der <<(Insertion) und der >>(Extraction) Operator. Denn beide habe wir schon häufig verwendet :

 

 

int Eingabe;
cin >> Eingabe;
cout << Eingabe;

Das heisst, dass bei diesen Eingabe-und Ausgabefunktionen jeweils der >>-Operator des Objektes cin und der <<-Operator des Objektes cout verwendet werden. Was für Objekte sind cin und cout eigentlich ? Wir kommen im nächsten Kapitel (Klassenableitung und Vererbung) dazu.

Klassenableitung und Vererbung

 

Versuchen wir das Prinzip anhand von uns bereits bekannten Klassen zu beschreiben. Zuerst wieder einmal unsere Klasse Auto ;-)

Angenommen wir haben eine Klasse Auto mit ein paar Eigenschaften, die für ein Auto allgemein gültig sind. Vielleicht verlangt aber unser Problem eine genauere Unterteilung in zum Beispiel Cabriolets, Sportwagen und Kombis. Dabei haben diese verschiedenen Auto-Typen gemeinsame Attribute wie Farbe und/oder Hubraum etc. Angenommen wir müssen eine Verkaufsdatenbank für Autos machen, dann hätten wir für ein Cabrio noch zusätzlich Angaben für die Windgeräusche. Beim Sportwagen interessiert möglicherweise eine Zulassung für Rennsport-Anlässe und ein Kombi hätte evtl. das maximal mögliche Ladevolumen als zusätzliche Angabe. Um diese Verwandtschaft auszudrücken bietet C++ die Möglichkeit der Vererbung.

 

 
class Auto
{
public:
Auto();
~Auto();
void Fahren();
private:
Farbe m_farbe;
double m_ccm;
};
class Cabrio : public Auto
{
public:
Cabrio();
~Cabrio();
private:
Dezibel m_wind;
};
class SportAuto : public Auto
{
public:
SportAuto();
~SportAuto();
private: bool m_rennen;
};
 

 

Nach dem Klassennamen wird einfach nach einem Doppelpunkt angegeben, von welcher Klasse dass abgeleitet werden soll. Das bedeutet dass die ein Objekt der Klasse Cabrio auch ein Auto ist. Alle Datenelemente, die zur Klasse Auto gehören sind auch in der Klasse Cabrio, SportAuto oder Kombi vorhanden. Diese Klassen sind jeweils Spezialisierungen der Basisklasse Auto. Folgender Code ist also gültig:

 

 

Cabrio einCabrio;
einCabrio.Fahren();
// Die Methode Fahren gehört zur Klasse // Auto, ist also auch für die Klasse
// Cabrio vorhanden.

 

 

Häufig wird auch folgendes Beispiel verwendet. Eine Personendatenbank basiert auf der Klasse Person.

Person.h

 

#ifndef PERSON_H
#define PERSON_H


#include <string>


using namespace std;

class Person
{
    public:
        Person();
        void SetName(const string& Name);
        void SetGeburi(int Tag, int Monat, int Tag);

    private:
        string m_Name;
        int    m_Tag;
        int    m_Monat;
        int    m_Jahr;
};

class Angestellter : public Person
{
    public:
        // Konstruktor mit Parametern
        // Das Datum ist der Tag an 
        // dem die Person angestellt wurde
        Angestellter(int Tag, int Monat, int Jahr);

    private:
        int m_StartTag;
        int m_StartMonat;
        int m_StartJahr;
};

#endif

 

 

Nun ist es also möglich eine Variable der Klasse Angestellter zu erzeugen und diese so zu verwenden wie ein Objekt der Klasse Person.

 

 

#include "Person.h"

int main()
{
Angestellter einAngestellter(3, 4, 2001);
einAngestellter.SetName("Fred Feuerstein");
einAngestellter.SetGeburi(1, 3, 1955);
return 0;
}

 

 

Zurüch zu cin, cout

Das Objekt cin ist von einer speziellen Klasse. Wir brauchen diese Klasse nicht zu kennen, wir können aber herausfinden, dass diese Klasse von der Klasse istream abgeleitet ist. Genauso ist ifstream auch von dieser Klasse abgeleitet !
Bei cout ist es ähnlich. cout ist ein Objekt von einer Klasse, die von ostream abgeleitet ist, genau wie die ofstream-Klasse, die wir für Ausgaben in eine Datei verwenden. Was man über diese Basisklassen auch aussagen kann, ist dass sie den << (Insertion)-Operator resp. den >>(Extraction) überschrieben haben !

 

 

Wie können wir das anwenden ?

Um den Kreis zu schliessen nehmen wir wieder die Klasse CD von zuoberst ! Es fehlt ja noch jegliche Eingabe als auch Ausgabe-Funktionalität ! Wir ergänzen sie mit folgender Funktion :

 

 

class CD
{
// gleicher Code wie oben aber
void WriteToStream(ostream& out);
};

Der cpp Code ist hier :

 

 

void CD::WriteToStream(ostream& out)
{
out << m_Title << endl;
out << m_Interpret << endl;
out << m_Length << endl;
}

 

 

Wir definieren dass die Funktion eine Referenz auf ein Objekt als Parameter nimmt, dass von der Klasse ostream abgleitet ist. Da das cout Objekt ein Objekt von der Klasse ostream ist, können wir cout also verwenden. Genauso können wir aber ein Objekt der Klasse ofstream verwenden, denn die Klasse ofstream ist ebenso von der Klasse ostream abgeleitet und die Klasse ostream hat den operator << überladen, den wir in der Funktion WriteToStream verwenden.

 

 

#include <fstream>  // für die file I/O Klassen
#include <iostream> // für cin, cout
#include "CD.h"

using namespace std;

int main()
{
ofstream fout("c:\\testCD.txt");

CD testCD;

testCD.WriteToStream(cout);
testCD.WriteToStream(fout);

return 0;
}