home | ### 1. Samstag
Druckversion dieser Seite |
| nachträgliche Ergänzungen findet Ihr hier.
### Zeigerarithmetik |
| Betrachte folgenden Code mit, der ein dynamisch erstelltes Array mit einer Schleife bearbeitet wird:
    int main()
    {
        // platz für dreissig characters
        char* buchstaben = new char[30];

        for(int i = 0; i < 30; ++i)
        {
            // jeweiligen character auf
            // den Wert 'A' setzen
            buchstaben[i] = 'A';
        }

        return 0;
    }
| Es ist möglich diesen Code auch anders zu schreiben, wobei ich auch gleich den Datentypen wechsle:
    int main()
    {
        // platz für zehn doubles
        double* werteStart = new double[10];

        double* werteEnde = werteStart + 10;
        // der Zeiger wertEnde erhält
        // den numerischen Wert
        // werte + (10 * 8) !

        for(double* wertAkt = werteStart;  // Initialisierung
            wertAkt != werteEnde;          // Laufbedingung
            ++wertAkt)                     // Reinitialisierung
        {
            *wertAkt = 3.5;
        }

        delete [] werteStart;

        return 0;
    }
| Kleine Beschreibung: Zuerst wird Platz für zehn double-Werte dynamisch auf dem Heap alloziert. Die zweite Codezeile zeigt ein erstes Beispiel von Zeigerarithmetik. Wir erzeugen den neuen Zeiger werteEnde und initialisieren ihn mit dem Wert von werteStart plus 10. Vorsicht ! Da der Datentyp vom werteEnde ein Zeiger auf double ist, rechnet der Compiler nicht einfach 10 zum Wert des Zeigers werteStart. Angenommen der Zeiger werteStart hätte den Wert 1000, erhält werteEnde nicht den Wert 1010. Der Compiler sorgt dafür, dass falls ein double-Zeiger wie werteStart auf eine double zeigt, dass ein Zeiger werteStart+1 auf den nächsten double im Speicher zeigt. Das heisst falls werteStart = 1000 ist werteStart+1 = 1008, denn ein double braucht 8 Bytes. In der Schleife wird in der Reinitialisierung der double Zeiger mit ++ heruafgezählt. Er wird also jedesmal so verändert, dass er auf den nächsten double im Speicher zeigt. Der Zeiger werteEnde zeigt in unserem Beispiel auf einen double, der gerade hinter unserem allozierten Speicher liegt. Man darf also an die Speicherstelle, auf die werteEnde zeigt nicht schreiben. Als Laufbedingung ist der Zeiger aber geeignet, die Schleife wird nämlcih rechtzeitig beendet. Überzeuge dich selbst mit dem Debugger davon ! Hier noch weitere Beispiele mit Zeigerarithmetik:
    int main()
    {
        // platz für dreissig characters
        double* werteStart = new double[10];

        double* werteEnde = werteStart + 10;
        // der Zeiger wertEnde erhält
        // den numerischen Wert
        // werte + (10 * 8) !

        // Zeiger auf zweites Element
        double* zweitWert = werteStart + 1;
        *zweitWert = 0.0;
        // andere Schreibweise :
        werteStart[1] = 5.5;

        // also
        bool esGeht = ( &werteStart[1] == zweitWert);
        // die Adresse des Elements mit index 1
        // ist gleich der Adresse des ersten
        // Elements + 1

        // Inhalt des letzten Elements auf 666
        // setzen
        *(werteStart+9) = 666;

        delete [] werteStart;

        return 0;
    }
| Wir werden eine ähnliche Art der Schleifenbildung wie oben verwenden, wenn wir mit den Containern und den Iteratoren der Standard-C++ Library arbeiten.
### Typedef |
| Typendefinitionen kennen wir bereits, denn wer wir eine eigene Klasse, eine eigene Struktur oder eine eigene union definieren, definieren wir einen Datentypen.
| Mit dem Schlüsselwort typedef können wir aus einem bestehenden Typen einen anderen definieren. Der zugrundeliegende Datentyp kann ein beliebiger eingebauter oder selbstdefinierter Typ sein. Der typedef wird häufig gebraucht um einem Datentypen einen anderen Namen zu geben, der entweder besser passt, oder auch hilft Tipparbeit zu sparen. Die Syntax ist wie folgt:
    typedef DATENTYP NEUE_BEZEICHNUNG;
| Hier ein, zwei Beispiele :
    class KlasseMitUnpraktischemNamen
    {
    public:
        KlasseMitUnpraktischemNamen()
        {
            m_zahl = 0;
        }
    private:
        int m_zahl;
    };

    //      Name des bestehenden Typs   neue Bezeichnung
    typedef KlasseMitUnpraktischemNamen ZahlKlasse;

    typedef int Anzahl;

    int main()
    {
        // Variable vom Typ
        // Zahlklasse
        ZahlKlasse element1;

        // Variable vom Typ
        // Anzahl;
        Anzahl eineAnzahl = 0;

        return 0;
    }
### Verwendung der Container-Klassen aus der STL
| Erinnern wir uns an die Klasse MyString, mit der wir die Verwendung von dynamischen Datenelementen kennegelernt haben. Wir haben dort die Daten in einem Array gespeichert, das wir dynamisch, das heisst bei Bedarf vergrössert haben. Das Ändern jenes Arrays war aufwendig und mit vielen Kopieroperationen verbunden.
| Das Problem, das wir eine sich ständig ändernde Anzahl von Elementen irgendwo speichern müssen kommt in C++ so häufig vor und wurde ebenso häufig falsch gelöst, dass man diese Probleme mit dem C++-Standard ein für allemal gelöst hat. Die C++ Standard Library (STL) enthält einige Klassen, um viele Elemente damit zu speichern. Man nennt diese Klassen Container. Unsere selbstprogrammierte CD-Liste ist ein Container für CD-Elemente.
| Mittels der template-Technik von C++ sind die Container-Klassen der STL für beliebige Datentypen anwendbar. Die Schreibweise wehen wir weiter unten.
| Man kennt in der Informatik einige bestimmte Container, die je nach Anwendung ausgewählt werden, da sie verschiedene Eigenschaften haben. Alle diese Container sind sehr ähnlich aufgebaut. Zum Beispiel erlauben die meisten davon mit der Funktion push_back neue Elemente hinzuzufügen.
| Ein vector ist ähnlich wie ein Array. Das heisst wenn wir Elemente (von irgend einem Datentypen) in einem vector speichern, können wir mittels eines index-operators beliebig auf die Elemente zugreifen. Beim vector besteht aber beim anfügen von neuen Elementen die Möglichkeit, dass der ganze vector wie in unserer MyString-Klasse ständig umkopiert wird, was manchmal unerwünscht ist.
main2.cpp |
    #include <iostream>
    #include <vector>

    using namespace std;

    // typedef zur besseren Lesbarkeit
    typedef vector<double> doubles;

    int main()
    {
        // ein doubles-Objekt erstellen
        doubles meineDoubles;

        // ein paar Werte hinzufügen
        meineDoubles.push_back(1.0);
        meineDoubles.push_back(1.1);
        meineDoubles.push_back(1.2);
        meineDoubles.push_back(1.3);
        meineDoubles.push_back(1.4);
        meineDoubles.push_back(1.5);

        int AnzahlWerte = meineDoubles.size();

        // Werte ausgeben
        for(int i = 0; i < AnzahlWerte; ++i)
        {
            cout << meineDoubles[i] << endl;
        }

        return 0;
    }
| Den Nachteil von der "herumkopiererei" hat die list nicht. Das hinzufügen eines neuen Elementes ist weniger aufwendig, wie wir in unserer CD-Liste nachsehen können. Dafür erlaubt sie keinen direkten Zugriff auf ein Element mitten in der Liste. Wir müssen uns zum gewünschten Element durcharbeiten.
| Dieser Zugriff auf die Elemente erfolgt bei der Liste mit einem sogenannten Iterator. Ein Iterator ist ähnlich wie ein Zeiger, den man mittels operator++ inkrementieren kann, so dass er auf das nächste Element im Container (man spricht auch von Kollektion) zeigt. Das Beispiel jetzt also mit einer Liste und mit einem Iterator.
main3.cpp |
    #include <iostream>
    #include <list>

    using namespace std;

    // typedef zur besseren Lesbarkeit
    typedef list<double> doubles;

    int main()
    {
        // ein doubles-Objekt erstellen
        doubles meineDoubles;

        // ein paar Werte hinzufügen
        meineDoubles.push_back(1.0);
        meineDoubles.push_back(1.1);
        meineDoubles.push_back(1.2);
        meineDoubles.push_back(1.3);
        meineDoubles.push_back(1.4);
        meineDoubles.push_back(1.5);

        // durch die ganze Kollektion
        // "durchwaten" mit Iteratoren

        // einen Iterator auf das erste Element
        // wie ein Zeiger auf das erste Element
        doubles::iterator akt = meineDoubles.begin();
        // und einer auf das Ende
        doubles::iterator end = meineDoubles.end();

        for(akt = meineDoubles.begin(); akt != end; ++akt)
        {
            double Wert = *akt;
            cout << Wert << endl;
        }

        return 0;
    }
| Übrigens funktioniert dieser Code auch mit vector als Container. Auch bei der Klasse vector sind die Iteratoren definiert ! Das heisst nur durch ändern des Wortes list auf vector funktioniert der Code oben genau gleich ! Der Programmierer muss für den jeweiligen Fall die Klasse auswählen, die besser passt.
| Ein stack (übersetzt ein Stapel) ist ein wenig anders aufgebaut. Man legt etwas zuoberst auf den Stack und kann immer nur auf das oberste Element zugreifen. Dieses Verhalten wird in der Computertechnik häufig angewendet um kurzzeitig etwas (eine Variable oder Daten) kurzzeitig abzulegen. Ein UPN Taschenrechner funktioniert auch mit einem Stack. Man legt die Zahlen auf den Stack und wählt dann die Funktion, die sich die benötigten Operatoren vom Stack abholt und das Ergebnis wieder dorthin zurücklegt. Hier zuerst ein kleines Beispiel für die Anwendung des Stacks:
main.cpp |
    #include <stack>

    using namespace std;

    int main()
    {
        // stack für doubles anlegen
        stack<double> doubleStack;

        // einen Wert auf den Stack
        // legen
        doubleStack.push(3.5);

        // Stackgrösse zur Kontrolle
        int stackSize = doubleStack.size();

        // oberstes Element holen
        // Das Element bleibt aber
        // auf dem Stack
        double d = doubleStack.top();

        // Stackgrösse zur Kontrolle
        stackSize = doubleStack.size();

        // oberstes Element entfernen
        doubleStack.pop();

        // Stackgrösse zur Kontrolle
        stackSize = doubleStack.size();

        return 0;
    }
| Das folgende Beispiel zeigt die Anwendung des Stacks für einen Rechner ! Versuche das Beispiel nachzuvollziehen !
main4.cpp |
    #include <iostream>
    #include <string>
    #include <strstream>
    #include <stack>

    using namespace std;

    int main()
    {
        // stack für doubles anlegen
        stack doubleStack;

        // eingabe initialisieren
        string eingabe = "";

        // mit x Schleife verlassen
        while(eingabe != "x")
        {
            cin >> eingabe;

            // addieren ?
            if(eingabe == "+")
            {
                // addieren nur falls
                // mindestens 2 Elemente
                // auf dem Stack sind
                if(doubleStack.size() >= 2)
                {
                    // oberstes Element holen
                    double op1 = doubleStack.top();
                    // oberstes Element entfernen
                    doubleStack.pop();

                    double op2 = doubleStack.top();
                    doubleStack.pop();

                    // rechnen
                    double ergebnis = op1 + op2;
                    // ergebnis zurück auf den
                    // Stack
                    doubleStack.push(ergebnis);
                    // Ausgabe zu Kontrolle
                    cout << ergebnis << endl;
                }
            }
            else
            {
                // strstream ist ein
                // stream, der einfach
                // im Speicher ist, sonst
                // aber ähnlich ist wie
                // cin oder cout
                strstream stream;

                // die Eingabe in den Stream
                stream << eingabe;

                // und als double wieder
                // aus dem Stream lesen
                double wert;
                stream >> wert;

                // Wert auf den Stack
                doubleStack.push(wert);
            }
        }

        return 0;
    }