Arrays und Pointer



  • Ich hab mal was zu diesem Thema geschrieben, vielleicht hilft es Dir weiter:
    Arrays und Pointer

    Stellen Sie sich ein Array zunächst als eine Straße mit Häusern nur auf einer Seite vor. In je-dem Haus wohnt eine Variable (z.B. Zeichen oder Zahl). Man kann ein Array auch als an-ein-an-der gereihte Kästchen sehen, die man mit Daten füllen kann. Nachfolgend finden Sie ein Array mit zehn Kästchen, von denen fünf mit den Buchstaben des Wortes HALLO belegt sind:

    H  A  L  L  O \0 
    [0][1][2][3][4][5]
    

    Die ersten fünf Kästchen sind klar, aber wer bewohnt denn da das sechste Kästchen? Dieses Zeichen, nämlich die binäre Null (ASCII-Code 0), beendet in C++ einen String. Ebenso könn-ten z.B. auch Zahlen bzw. Zahlen und Buchstaben im Array stehen. Die "Bewohner" der "Kästchen" nennt man auch die Elemente des Arrays. Arrays bezeichnet man auch als Felder oder Vektoren (eindimensional) bzw. Tabellen (mehrdimensional).

    Ein Array ist also ein Feld mit einer festgelegten Anzahl von Elementen des gleichen Typs (char, int, float, double etc.). Der Typ der Elemente und damit der Speicherbedarf pro Element wird bei der Deklaration, z.B. char eingabe[80], fest-gelegt. In der eckigen Klammer steht die Gesamtzahl der Elemente.

    Wichtig:
    Die Zählung der Elemente eines Arrays in C++ beginnt nicht mit [1], sondern mit [0]. Das erste Element ist z.B. eingabe[0] und das letzte Element eingabe[79].

    Beispiel: Bei der Programmierung von Schleifen durchläuft man 80 Elemente, indem man den Schleifenzähler i nicht von 1 bis 80, sondern von 0 bis 79 zählt.

    In den nachfolgenden kleinen Programmen lesen wir mittels der Funktion cin einen Zeichenstring ein. Diese Strings bestehen aus einzelnen Zeichen, bei Worten sind dies Buchstaben, die als Elemente in das Array eingabe[80] eingehen. Dieses Array umfaßt gemäß Deklaration Speicherplatz für 80 Elemente des Typs char. Wir werden den Umgang mit den Strings, die in dem Array gespeichert sind, nun trainieren, damit die Wirkung von Adressen und Zeigern klarer wird.

    Als Beispiel benutze ich hier das Wort "Jahrhundert". Die Funktion cin liest den String ein und speichert ihn im Array eingabe [80]. Im Speicher befinden sich daraufhin folgende Daten des Typs char:

    |J|a|h|r|h|u|n|d|e|r|t|\0|

    Der String wird durch "\0" abgeschlossen, das heißt wir können im Array eingabe[80] mit seinen insgesamt 80 Elementen des Typs char maximal 79 Zeichen (Character) gefolgt von der binären Null ablegen. Die einzelnen Elemente des Strings (also die ein-zel-nen Zeichen) können direkt über den Namen eingabe[i] angesprochen werden. In eingabe[0] findet sich z.B. das Zeichen 'J'. Hier werden gezielt Hochkommas benutzt, da Anführungs-zeichen für Zei-chen-ketten (Strings) eingesetzt werden, und diese beinhalten an ihrem Ende "\0".

    Jedes Element hat im Array eine Nummer, z.B. die [0] für 'J'. Die Variable eingabe[0] hat jedoch auch eine Adresse im Speicher des Computers.

    Die Speicheradresse des "Kästchens" eingabe[0] findet man über &eingabe[0]. Das voran-gestellte & (Adress-Operator) vor dem Element des Arrays stellt sicher, daß wir uns nicht auf den Inhalt, sondern auf die Speicheradresse des "Kästchens" beziehen. Zusätzlich erlaubt C++ hier eine gleichwertige Schreibweise, die jedoch am Anfang eher zur Verunsicherung beiträgt. Durch Weglassen der eckigen Klammern wird der Name des (eindimensionalen) Arrays zur Anfangsadresse:

    &eingabe[0] ist die Speicheradresse von eingabe[0] und kann äquivalent als
    eingabe geschrieben werden.

    Zusätzlich bringen wir an dieser Stelle noch den Inhalts-Operator * (auch indirection-Operator oder Dereferenzierungs-Operator genannt), der uns den Inhalt einer Adresse übergibt:

    *&eingabe[0] ist der Inhalt der Adresse und daher gleichwertig mit eingabe[0]

    Jetzt betrachten wir dies in der Programmierpraxis. Das nachstehende Programm (array001) kann uns helfen, den Aufbau von Arrays zu verstehen. Wir geben z.B. den Zeichenstring "Jahrhundert" in eingabe[80] ein und belegen damit inclusive \0 die ersten 12 Plätze also die Elemente 0 ... 11 des Arrays. Die for-Schleife gibt nun beginnend (i=0) mit eingabe[0] nach-einander die einzelnen Elemente eingabe[i] aus. Die Bedingung eingabe[i]!='\0' beendet die for-Schleife, sobald das Zeichen '\0' gefunden wird. '\0' wird nicht mehr ausgegeben.

    //Programm array001.cpp
    #include <iostream>
    using namespace std;
    
    int main()
    {
        char eingabe[80];
    
        cin >> eingabe;
    
        for (int i = 0; eingabe[i] != '\0'; ++i) // != bedeutet ungleich
            cout << eingabe[i];
    
        return 0;
    }
    

    Jetzt ändern wir eingabe[i] in *&eingabe[i] ab, um die Wirkung von "Inhalt der Adresse ..." zu überprüfen.

    //Programm array002.cpp
    ...
    cout << *&eingabe[i]; // identisch mit eingabe[i] 
    ...
    

    Bei beiden Programmen erhalten wir folgende Bildschirmausgabe:
    Jahrhundert (eingegebener String)
    Jahrhundert (ausgegebener String)

    Sie haben damit Grundfunktionen kennengelernt, um einen String in ein Array einzulesen und aus diesem wieder vollständig auszulesen.

    Ändern Sie das Programm jetzt wie folgt ab:

    //Programm array003.cpp
    #include <iostream>
    using namespace std;
    
    int main()
    {
        char eingabe[80];
    
        cin >> eingabe;
    
        for (int i = 0; eingabe[i] != '\0'; ++i)
            cout << *eingabe;
    
        return 0;
    }
    

    Jahrhundert
    JJJJJJJJJJJ

    Was bedeutet *eingabe noch einmal genau? eingabe war identisch mit &eingabe[0]. Damit wird *eingabe zu *&eingabe[0]. Der Inhalt an der Speicheradresse des nullten Elements ist das nullte Element und damit 'J'. Dieses Element wird im Programmablauf solange aus-gegeben, bis die for-Schleife auf das Stringende '\0' stößt.

    Fassen wir kurz zusammen, was wir bisher mittels cout in der for-Schleife ausgegeben haben:

    eingabe[i] Elemente des Arrays
    *&eingabe[i] Inhalte der Adressen der Elemente des Arrays (=Elemente des Arrays)
    *eingabe Inhalt der Startadresse des Arrays (=nulltes Element des Arrays)

    Jetzt testen wir den Adress-Operator ohne Aufhebung durch den Inhalts-Operator:

    // Programm array004.cpp
    #include <iostream>
    using namespace std;
    
    int main()
    {
        char eingabe[80];
    
        cin >> eingabe;
    
        for (int i = 0; eingabe[i] != '\0'; ++i)
            cout << &eingabe[i];
    
        return 0;
    }
    

    Jahrhundert
    Jahrhundertahrhunderthrhundertrhunderthundertundertndertdertertrtt

    Ja, das sieht ja lustig aus! Eigentlich hatten wir unter Benutzung des Adress-Operators erwartet, daß die Speicher-adressen der Elemente des Arrays nacheinander ausgegeben werden. Stattdessen erhält man den gesamten String ab der übergebenen Adresse. Interessant, aber zunächst unlogisch. Sie vermuten, daß dies mit einer Sonderbehandlung von Strings durch C++ zusammenhängt? Es war ja bereits merkwürdig, daß wir mit cin unter Angabe der Startadresse, also eingabe (gleichbedeutend mit &eingabe[0]) den gesamten String über-nehmen konnten.

    Um diesen Zusammenhang vollständig zu verstehen, wiederholen wir das Ganze in gleicher Form mit einem Integer-Array.

    // Programm array005.cpp
    #include <iostream>
    int main()
    {
        int eingabe[80];
    
        cin >> eingabe;
    
        for (int i = 0; eingabe[i] != '\0'; ++i)
            cout << &eingabe[i];
    
        return 0;
    }
    

    Hier meldet der Compiler bei cin>>eingabe eine "Illegal structur operation". Da wir wissen, daß eingabe die Kurzform für &eingabe[0] ist, tauschen wir dies aus und erhalten:

    // Programm array006.cpp
    ...
    cin >> &eingabe[0];
    ...
    

    Auch hier meldet der Compiler an der Stelle cin>>eingabe eine "Illegal structur operation". Dies ist beruhigend, da wir ja nur den gleichwertigen Programm-Code angewandt haben. Nun wird es klarer: Die Fähigkeit von cin (oder scanf in C), einen String unter Angabe einer Adresse der Reihe nach als Character-Variablen in ein Array des Typs char einzulesen, ist etwas besonde-res, aber leider nicht von glasklarer Logik geprägt.

    Da wir jetzt endlich Adressen sehen möchten, streichen wir den Adress-Operator und geben separat die ersten drei Elemente des Arrays ein:

    // Programm array007.cpp
    #include <iostream>
    using namespace std;
    
    int main()
    {
        int eingabe[80];
    
        cout << "Erste Zahl: ";
        cin >> eingabe[0];
        cout << "Zweite Zahl: ";
        cin >> eingabe[1];
        cout << "Dritte Zahl: ";
        cin >> eingabe[2];
        cout << "\n";
    
        for (int i = 0; i < 3; ++i)
            cout << &eingabe[i] << "\t" << eingabe[i] << "\n";
    
        return 0
    }
    

    Wenn Sie jetzt z.B. drei Integer-Zahlen eingeben, dann werden links die Speicheradressen und rechts daneben die zuvor eingegebenen Zahlen auf dem Bild-schirm dargestellt (die Speicheradressen können bei Ihnen andere sein):

    0x1b56 555
    0x1b5a 444
    0x1b5e 333
    

    Beachten Sie bitte, daß wir einiges ver-än-dert haben, um ein vernünftiges Programm zu erhalten. Insbesondere haben wir die Zahlen einzeln nacheinander abgefragt (kein String), und der Abbruch-Trick unter Prüfung des Elementes auf '\0' in der for-Schleife ist hier sinnlos.

    Wir gingen hier bewußt den Weg vom String im Character-Array zu Zahlen im Integer-Array, da-mit deutlich wird, daß Besonderheiten bei der String-Ein- und -Ausgabe in Verbindung mit & und *& bestehen. Leider gehen diese bezüglich des Adress-Operators auf Kosten der Logik. Das sehen wir uns später noch genauer an.

    Jetzt verbleiben wir in der glasklaren Welt der Zahlen und experimentieren mit dem Inhalts-Operator, indem wir eingabe[i] durch *&eingabe[i] ersetzen. Zusätzlich geben wir aus Neu-gier einfach drei weitere Speicherplätze des Arrays aus, die wir nicht selbst vorher mit Zahlen be-schicken:

    // Programm array008.cpp
    #include <iostream>
    using namespace std;
    
    int main()
    {
        int eingabe[80];
    
        cout << "Erste Zahl: ";
        cin >> eingabe[0];
        cout << "Zweite Zahl: ";
        cin >> eingabe[1];
        cout << "Dritte Zahl: ";
        cin >> eingabe[2];
        cout << "\n"
    
        for (int i = 0; i < 6; ++i)
            cout << &eingabe[i] << "\t" << *&eingabe[i] << "\n";
    
        return 0;
    }
    

    Hier funktioniert der Adress-Operator wie erwartet. Er gibt die Speicheradresse der Elemente eingabe[i] aus. Insbesondere sehen wir an den Anständen zwischen den Hexadezimal-Adres-sen, daß eine Integer-Va-riab-le 4 Bytes benötigt. Jetzt steigen wir um auf float-Variablen und sehen, daß solcheVa-riablen 4 Bytes belegen. Zusätzlich prüfen wir noch einmal bezüglich *&eingabe[i] und ein-gabe[i] auf Übereinstimmung. Geben Sie bitte Zahlen mit mehr als 6 Stellen nach dem Komma ein, um die hö-he-re Genauigkeit von double im Vergleich zu float zu testen:

    // Programm array009.cpp
    #include <iostream>
    using namespace std;
    
    int main()
    {
        float eingabe[80];
    
        cout << "Erste Zahl: ";
        cin >> eingabe[0];
        cout << "Zweite Zahl: ";
        cin >> eingabe[1];
        cout << "Dritte Zahl: ";
        cin >> eingabe[2];
        cout << "\n";
    
        for (int i = 0; i < 6 ; ++i)
            cout << &eingabe[i] << "\t" << *&eingabe[i] << "\t" << eingabe[i] << "\n" ;
    
        return 0;
    }
    

    Nehmen Sie die Gelegenheit wahr und testen auch double (8 Byte) und long double (10 Byte) auf Speicherplatzbedarf und Genauigkeit.

    Nachdem wir gesehen haben, daß C++ im Zahlenbereich bezüglich des Adress-Operators wie er-wartet reagiert, kehren wir zu den Strings zurück. Hier besteht nach wie vor die Aufgabe, Spei--cheradressen einzelner Elemente eines Strings am Bildschirm auszugeben. Wir gehen von Programm array004 aus und wandeln die Adresse &eingabe[i] vor der Ausgabe in einen Zah-len-wert des Typs long um:

    //Programm array010
    #include <iostream>
    using namespace std;
    
    int main()
    {
        char eingabe[80];
    
        cin >> eingabe;
        cout << "\n";    
    
        for (int i = 0; eingabe[i] != '\0'; ++i)
            cout << static_cast<void *>( &eingabe[i] ) << "\n";
    
        return 0;
    }
    

    Wie man sieht, haben wir jetzt das Ziel erreicht, die Speicheradressen der einzelnen Zeichen aus-zugeben.
    Jetzt sollten wir erforschen, was char* und void * beschreibt und warum es bei der Ausgabe mit cout verschieden reagiert?

    Nach den obigen Versuchen mit Arrays und Strings kommen wir jetzt zu den Zeigern. Arrays, Strings und Zeiger (Pointer) sind in C++ eng miteinander verknüpft. Daher betreten wir jetzt dieses interessante Gebiet der Zeiger, das für viele abschreckend wirkt.

    Zuvor fassen wir kurz zusammen:
    Bisher experimentierten wir mit einzelnen Array-Elementen eingabe[i], den zugehörigen Spei-cheradressen &eingabe[i] (Besonderheit: eingabe ist &eingabe[0]) und den Inhalten an den Speicher-adressen *&eingabe[i]. Wir fanden, daß man bei Elementen in Strings die Ausgabe der Spei-cher-adressen durch Umwandlung in eine Integer-Variable erreicht. Bei Ausgabe der "nackten" Adresse wurde nicht die Adresse, sondern der restliche String ab dieser Adresse aus-ge-geben (interessant, aber zunächst nicht be-ab-sichtigt und noch nicht völlig verstanden).

    Original erstellt von <erklärer>:
    also ein zeiger (ich red deutsch):
    in einer "normalen" variable (int, double, char, ...) speicherst du einen Wert.
    in einem Zeiger kannst du genauso einen Wert speichern, nur sollte dieser Wert eine Adresse im Speicher sein. Über den Zeiger kannst du dann den Inhalt des Speichers, auf den diese Speicheradresse verweist, ändern

    Beispiele (kompilierbar)
    [cpp]
    #include <iostream>
    using namespace std;

    int main () {
    {
    int x = 10; //Erzeuge eine Variable im Stapelspeicher (Stack) des Programms
    //vom Typ int, und weise ihr den Wert 10 zu
    int *zeiger;//Erzeuge einen Zeiger, der nur Adressen von ints beinhalten kann
    zeiger = &x;//&x - nimm die Speicheradresse von x und weise sie dem Zeiger zu
    //Man sagt: "zeiger" zeigt auf x

    cout << x << endl;
    cout << *zeiger << endl; //Mit dem Dereferenzierungsoperator * greifst du
    //auf den Inhalt der Speicheradresse, die im Zeiger
    //gespeichert ist zu

    (*zeiger)++; //Inkrementiere den Wert, auf den der Zeiger zeigt um 1
    cout << x << endl; //x ist inkrementiert worden
    }
    //2. Beispiel:
    {
    int x = 10, y = 20; //Zwei neue ints auf dem Stack erzeugen
    int * zeiger; //Erklärung: siehe oben
    int * * doppel_zeiger;//Erzeuge einen Zeiger, der nur auf Zeiger zeigen kann,
    //die auf ints zeigen

    zeiger = &x; //zeiger zeigt auf x
    doppel_zeiger = &zeiger; //doppel_zeiger zeigt auf zeiger

    (*doppel_zeiger) = &y; //lässt den zeiger, auf den doppelzeiger zeigt, auf y zeigen

    cout << *zeiger << endl; //beweis: zeiger zeigt auf y

    cout << **doppel_zeiger << endl; //Doppelte Zeiger müssen auch doppelt
    //dereferenziert werden, um den richtigen
    //wert zu erhalten

    }
    //3. Beispiel - Zusatz - 3/4/5 fach Zeiger (hoffentlich vertippe ich mich nicht)
    {
    char x = 'x', y = 'y', z = 'z'; //Natürlich gibt es nicht nur int-Variablen
    char *zeiger1 = &x, *zeiger2 = &y; //Merke die Syntax:
    /*
    int *i, y; - i ist vom Typ Zeiger auf int, y vom typ int!
    int i, *y; - i ist vom Typ int, y vom Typ Zeiger auf int!
    int *i, *y; - i und y sind vom Typ Zeiger auf int
    */
    char * * zeiger_auf_zeiger = &zeiger1;
    **zeiger_auf_zeiger = 'a';
    cout << x << endl; //Sollte 'a' ausgeben

    zeiger_auf_zeiger = &zeiger2; //Merken für unten
    **zeiger_auf_zeiger = 'b';
    cout << y << endl; //Sollte 'b' ausgeben

    *zeiger_auf_zeiger = &z;
    cout << *zeiger2 << endl; //Sollte 'z' ausgeben
    **zeiger_auf_zeiger = 'c';
    cout << z << endl; //Sollte 'c' ausgeben

    char *** dreifach_zeiger = &zeiger_auf_zeiger;
    ***dreifach_zeiger = 'z';
    cout << z << endl; //Sollte 'z' ausgeben
    **dreifach_zeiger = zeiger1;
    ***dreifach_zeiger = 'n';
    cout << x << endl; //Sollte 'n' ausgeben

    So, 4 und 5fach Zeiger darf wer anders machen!
    }
    //4. Beispiel - Die häufigere Verwendung von Zeigern - dynamischer Speicher
    {
    int *x = new int; //operator new nimmt sich Speicher vom Heap (Freestore, Freispeicher, etc)
    *x = 10;
    //und nicht vergessen:
    delete x; //Um den Speicher wieder freizugeben
    }
    //5. Beispiel - Array auf dem Heap
    {
    int *x = new int[10]; //Hole Speicher für 10 ints
    //x (und im allgemeinen Zeiger überhaupt) kann wie ein Array verwendet werden
    x[0] = 1;
    x[1] = 2;
    ...
    x[9] = 10; //10. Element ist unter x[9]

    //Freigeben mit delete []
    delete [] x; //wenn du mit new [] speicher besorgst, mit delete [] freigeben!
    }
    //6. Beispiel - Zeigerarithmetik - Einführung
    {
    /*Man kann auch noch anders auf array-elemente zugreifen: du inkrementierst den zeiger:*/
    int *x = new int[10];
    *x = 1; //hat denselben effekt wie x[0] = 1;
    ++x; //x zeigt jetzt einfach auf den nächsten int im Speicher, also auf &x[1]
    *x = 2; //etc.
    --x; //wieder zurück, den zum freigeben muss x auf das erste element zeigen
    delete x;
    }
    //7. Beispiel - Arrays und Zeigerarithmetik - Vertiefung
    {
    int x[10]; //Ein Array von 10 ints herstellen
    int *y = &x[0]; //y zeigt auf das 1. Element von x
    *y = 10;
    cout << x[0] << endl; //Ausgabe: '10'
    ++y; //y zeigt auf das 2. Element von x
    *y = 20;
    cout << x[1] << endl; //Ausgabe: '20'

    int *z = x; //Wie oben schon besprochen: eine Array-Variable (x) verhält sich
    //wie ein zeiger (z)
    z += 5; //Um 5 ints weiterspringen -> Zum 5.Element
    *z = 50;
    cout << x[4] << endl; //5.Element ausgeben: sollte sein '50'
    z -= 4; //Auf das 2. Element zurückspringen
    (*z)++;
    cout << x[1] << endl; //Ausgabe: 21

    /*
    Wie man sieht, kann man zu Zeigern also auch Werte addieren, und so quer
    durch den Speicher navigieren. Wenn ein Zeiger aber auf eine "falsche"
    Adresse zeigt (z.b. hinter die Array-Grenze) oder auf 0 Zeigt (int *x = 0)
    und man in dereferenziert, erzeugt dies "undefiniertes Verhalten".
    Der Computer könnte abstürzen, oder auch ein Vogonenraumschiff anrufen,
    und den Kommandanten an seine Mission, die Erde zu vernichten, erinnern,
    was er vergessen haben könnte, weil er gerade ein Gedicht vorträgt.
    Also: Zeiger zu dereferenzieren, die auf ungültige Adressen verweisen, ist
    *schlecht
    /

    //8. Beispiel - Zeigerarithmetik (3, oder shon 4?) - ptrdiff_t
    {
    //Man kann auch Zeiger (gleichen typs versteht sich)
    //voneinander abziehen. Das Ergebnis hat den Typ ptrdiff_t:

    int x[10];
    ptrdiff_t groesse = &x[9] - &x[0]; //groesse enthält nun den Wert, wieviele
    //ints zwischen x[0] und x[9] liegen.
    }
    //9. Beispiel - Dereferenzierung und der operator ->
    {
    //C++ ist eine schöne objektorientierte Sprache, in der man natürlich
    //auch Zeiger auf Objekte anlegen kann
    struct bar {
    int x, y;
    bar () : x(0), y(0) {}
    };
    bar b;
    b.x = 10; b.y = 20; //Alles klar, bis jetzt.

    //Und nun erzeugen wir ein bar im Heap:
    bar *b = new bar;
    //Wie greift man jetzt auf die elemente zu? richtig, zuerst dereferenzieren:
    (*b).x = 10; (*b).y = 20;
    //und wieder löschen
    delete bar;

    //Und das ganz nochmal als Beispiel mit nem Array
    bar *barr = new bar[20];
    barr[0].x = 10; barr[0].y = 20;
    /* etc. */
    delete [] barr;

    //Aber - weil sich jemand gedacht hat, er müsse zuviel tippen - ist die schreibweise
    //(*zeiger).member zwar erlaubt, aber durch die kürzere Form
    //zeiger->member ersetzbar:
    bar *foo = new bar;
    foo->x = 10; foo->y = 20;
    delete foo;
    }

    } //ende von main[/cpp]

    So. Ich nenne das eine kleine Einführung in die Welt der Zeiger. Das klingt jetzt ganz schön verwirrend, für einen Neuling, aber so ist's. Ich hoffe ich habe keine Fehler eingebaut (und wenn doch, bitte korrigiert mich).
    Was ich noch sagen will: Das ist mir jetzt mal so auf die schnelle eingefallen. Ich wette, es gibt noch mindestens 1000 Probleme, die noch auf dich zukommen, Neuling 😉 Aber dazu gibt es ja die Community 🙂
    Ich will auch nicht demotivierend klingen, aber das ist noch nicht mal ein kleiner Teil der Komplexität von C++. Viel Spaß noch beim lernen 😃
    Ich hoffe, ich habe ein kleines bisschen weitergeholfen.


Anmelden zum Antworten