[X] Pointer in C(++)



  • Michael E. schrieb:

    Thx. Dir auch (kein Plan, wo Rottweil ist und was man da machen könnte).

    Rottweil ist unterhalb von Stuttgart und man kann da fast gar nichts machen(sagt meine Erfahrung dieser Woche), hat schätzungsweise so um die 15000 Einwohner. Der LIDL ist schon n Höhepunkt 😞
    Allerdings hab ich irgendwo ein Etablissement gesehen 😉 😉



  • Was ist denn jetzt mit dem Artikel?

    1. Ist der GANZ fertig? Wird nichts mehr hinzugefügt?
    2. Wurde der schon korrigiert?
    3. Soll ich ihn (nochmals) korrigieren?

    Mr. B



  • Mr. B schrieb:

    1. Ist der GANZ fertig? Wird nichts mehr hinzugefügt?

    Ja, ist ganz fertig.

    Mr. B schrieb:

    2. Wurde der schon korrigiert?

    Teilweise, am ersten Post von Michael E. siehst du wie weit.

    Mr. B schrieb:

    3. Soll ich ihn (nochmals) korrigieren?

    Wenn du den Rest machen würdest, das wäre sehr nett.

    thanks in advance

    GPC



  • okay. mache ich dann voraussichtlich morgen. hab heute mit der korrektur von marc++us' artikel schon genügend getan! 😉

    Mr. B



  • Ist gut, lass dir ruhig Zeit.



  • Dieser Artikel wird sich mit Zeigern (engl.: Pointer) in C bzw. C++ beschäftigen. Der Quellcode wurde mit dem g++ 3.3.6 getestet. Sollten sich trotzdem irgendwelche groben Fehler eingeschlichen haben, bitte ich um Mitteilung.

    Folgendes wird behandelt:

    1. Einführung
    2. Zugriff auf Daten durch Pointer
    3. Übergabe/Rückgabe von Pointern
    3.1. Exkurs Referenzen
    4. Zeigerarithmetik
    5. const bei Pointern
    6. Speicher allokieren
    7. Pointer auf Pointer

    ######################################################################
    1. Einführung
    ######################################################################

    Viele verteufeln sie als fehleranfällig und kompliziert, aber wir brauchen sie trotzdem: Pointer, z.B. um Datenstrukturen wie Listen oder Bäume effizient zu implementieren oder einfach um das Kopieren eines Objekts bei einer Übergabe zu vermeiden.
    Für Einsteiger ist das Durcheinander der Sternchen und &-Zeichen meistens recht verwirrend. Mal sehen, ob wir etwas Klarheit reinbringen können...

    Zuerst werden wir uns anhand eines kurzen Beispiels anschauen, was normale Variablen und Pointervariablen im Speicherbild unterscheidet:

    #include <iostream>
    
    using namespace std;
    
    int main(int argc, char **argv) {
      int i = 5;  //int Variable
      int *p = 0;  //Pointer auf int, mit 0 initialisert!
      p = &i;  //mit Operator & Adresse von i holen und an p zuweisen
      cout<<"Adresse von i: "<<&i<<'\n';
      cout<<"Wert von p: "<<p<<'\n';
      cout<<"Adresse von p: "<<&p<<'\n';
      return 0;
    };
    

    Bei mir ergeben sich folgende Werte: Variable i hat den Wert 5 und die Adresse 0xbffff514, Pointer-Variable p hat den Wert 0xbffff514 (Adresse von i) und selbst die Adresse 0xbffff510!

    Vereinfacht gesagt ist ein Pointer auch nur eine Variable im Hauptspeicher, die eben eine Adresse als Wert hat.

    Im obigen Beispiel haben wir gesehen, dass man einen Zeiger auf int mit dem *-Zeichen deklariert und an die Adresse eines Objekts mit dem &-Operator kommt. Übrigens sollte ein Zeiger auf int auch nur auf ints zeigen und nicht auf andere Typen wie double!
    Allerdings haben wir dem Zeiger zuerst die Adresse 0 zugewiesen. Dies haben wir getan, weil die 0 bei Zeigern eine ganz spezielle Bedeutung hat, nämlich dass der Zeiger momentan ungültig ist bzw. auf nichts Gültiges zeigt (der Standard garantiert, dass gültige Adressen ungleich 0 sind). D.h. man kann und sollte einen Zeiger vor Benutzung auf 0 prüfen, um ein Segmentation Fault zu vermeiden.

    Wenn ein Zeiger bei der Deklaration noch nichts referenzieren kann oder soll, dann ist es sehr***,*** sehr empfehlenswert***,*** ihn vorbeugend mit 0 zu initialisieren.
    Man kann anstatt 0 auch die symbolische Konstante NULL verwenden - das ist gleichwertig und kann auf den ersten Blick offensichtlicher machen, dass mit Zeigern gearbeitet wird. Ob man jetzt 0 oder NULL schreibt***,*** bleibt jedem selbst überlassen. Für den Artikel werde ich 0 verwenden.

    ######################################################################
    2. Zugriff auf Daten durch Pointer
    ######################################################################

    Wenn der Wert eines Zeiger die Adresse einer Variablen ist, dann muss man auch an den Wert der Variablen selbst herankommen. Das ganze nennt sich Derefenzieren und geschieht mit dem *-Operator:

    #include <iostream>
    
    using namespace std;
    
    int main(int argc, char **argv) {
      int i = 5, j;
      int *p = &i;	//mit Operator & Adresse von i holen und an p zuweisen
      j = *p;  //Derefenziere p mit Operator * und weise Ergebnis j zu
      cout<<j<<'\n';  //Gibt 5 aus
    
      p = &j;  //p zeigt jetzt auf j
      *p = 215;  //p dereferenzieren und 215 zuweisen
      cout<<j<<'\n';  //Gibt 215 aus
      return 0;
    };
    

    Hier sieht man, dass p nur einen Verweis auf i (und dann auf j) darstellt. Wenn man also p dereferenziert und ändert, dann ändert man auch die Variable, auf die p zeigt.

    Strukturen und Klassen besitzen meistens Datenelemente. Da es möglich ist***,*** Zeiger auf Strukturen zu bilden, muss es auch möglich sein***,*** über den Zeiger auf die Datenelemente zuzugreifen, und zwar so:

    #include <iostream>
    
    using namespace std;
    
    //Billig-Wrapper für ints, müssten natürlich noch Operatoren +, += usw. dazu
    class Integer {
    public:
      explicit Integer(int i=0) : data(i) {}
      Integer(const Integer &i) : data(i.data) {}
      ~Integer() {}
    
      inline void setData(int i) { data = i; }
      inline int getData() const { return data; }
    
      Integer& operator=(int i) {
        data = i;
        return *this;
      }
    
      Integer& operator=(const Integer &i) {
        if (this == &i)
          return *this;
    
        data = i.data;
        return *this;
      }
    private:
      int data;
      operator int () const;
    };
    
    int main(int argc, char **argv) {
      Integer myInt(7);
    
      Integer *pi = &myInt;
      (*pi).setData(12);
      cout<<pi->getData()<<'\n';  //Wir benutzen den Pfeil-Operator
      return 0;
    };
    

    Prinzipiell ist es egal, ob man die erste oder zweite Schreibweise wählt, aber die zweite ist doch wesentlich komfortabler und offenbart gleich, dass man mit einem Zeiger arbeitet.

    Mit einem Zeiger auf ein Objekt kann man ein nettes Spielchen spielen, das da heißt: Verstecke ein Objekt hinter einem Zeiger! Und so spielt man es: Zeiger sind unabhängig vom Typ, auf den sie zeigen, auf 32 Bit-Rechnern immer 4 Byte (die Speicherung einer Adresse braucht dort so viel Platz) groß. Wenn ich jetzt in meiner Klasse X die Membervariable Integer myInt habe, dann wird beim Kompilieren und einer vorangegangenen Veränderung von Integer X ebenfalls neu kompiliert, obwohl sich an X eigentlich nichts verändert hat. Wenn ich aber die Membervariable zu einem Zeiger auf Integer mache, dann wird X nicht neu kompiliert, denn an X selbst ändert sich nichts, egal was mit Integer passiert. Nett, oder?

    ######################################################################
    3. Übergabe/Rückgabe von Pointern
    ######################################################################

    Funktionen können Parameter haben, d.h. wir übergeben diesen Funktionen Daten***,*** mit denen sie dann arbeiten. Das tun wir entweder 'by Value' oder 'by Reference':

    #include <iostream>
    
    using namespace std;
    
    //Übergabe by Value, int Variable wird beim Funktionsaufruf kopiert
    int incrementByVal(int i) {
      return ++i;
    };
    
    //Übergabe by Reference, wir arbeiten direkt mit der int Variable
    void incrementByRef(int *p) {
      ++(*p);  //Dereferenzieren und erhöhen, wir erhöhen nicht den Zeiger selbst!
    };
    
    int main(int argc, char **argv) {
      int j=5;
      cout<<"j (by Value): "<<incrementByVal(j)<<'\n';  //Gibt 6 aus
    
      cout<<"j(vorher, by Ref): "<<j<<'\n';  //Gibt 5 aus
    
      //Da die Funktion einen Pointer auf int erwartet, übergeben wir eine Adresse!
      incrementByRef(&j);  
      cout<<"j(nachher, by Ref): "<<j<<'\n';  //Gibt 6 aus
    
      return 0;
    };
    

    Bei der Übergabe by Value erhalten wir eine Kopie der int-Variable und müssen dann das Ergebnis mittels return zurückgeben, d.h. wir zahlen dafür, dass die int-Variable kopiert werden muss. Gut, bei int ist es nicht viel, aber bei größeren, komplexeren Datenstrukturen (std::string ist schon zu groß) rechnet sich das auf alle Fälle.
    Bei der Übergabe by Reference arbeiten wir direkt mit dem Objekt***;*** es findet kein Kopieren und kein return statt. Wir zahlen gar nichts fürs Kopieren, fast nichts, denn jetzt können wir keine Ausdrücke wie j+5 mehr übergeben***. Dieser*** Ausdruck hat nämlich keine Adresse im Hauptspeicher und kann somit von einem Pointer nicht referenziert werden***. Außerdem*** braucht natürlich der Pointer selbst 4 Bytes.

    Natürlich kann man Zeiger auch als Return-Werte einsetzen, allerdings muss man unbedingt sicherstellen, dass die Variable, auf die der Zeiger verweist, auch nach dem Verlassen der Funktion noch existiert, sonst wird unser Programm beim Zugriff auf den Zeiger abschmieren:

    int * increment(int i) {
      int var = i;
      ++var;
      return &var;
    };  //hier wird var zerstört, wohin zeigt der Pointer jetzt??
    

    Wenn man jetzt versucht den Zeiger zu dereferenzieren, wird das Programm mit Sicherheit (irgendwann) mit einem Segmentation Fault abstürzen!

    Eine Lösung wäre var static zu deklarieren, um sicherzustellen, dass die Variable auch nach Verlassen der Funktion noch existiert:

    #include <iostream>
    
    using namespace std;
    
    int * increment(int i) {
      static int var = i;  //var ist jetzt static, bleibt also erhalten
      ++var;
      return &var;
    };
    
    int main(int argc, char **argv) {
      int j=5;
      int *p = increment(j);
      cout<<*p<<'\n';  //Gibt 6 aus
      cout<<j<<'\n';  //Gibt 5 aus
      return 0;
    };
    

    ######################################################################
    3.1. Exkurs Referenzen
    ######################################################################

    Im Zusammenhang mit der 'by Reference'-Übergabe möchte ich kurz Referenzen (nur C++) ansprechen, die den Zeigern zwar ähneln, aber keine sind. Der wichtigste Unterschied ist wohl***,*** dass Referenzen immer nur einen anderen Namen für das referenzierte Objekt darstellen und somit keinen eigenen Speicher belegen. Dass wiederum bedeutet***,*** dass es nicht möglich ist, Zeiger auf Referenzen, wohl aber Referenzen auf Zeiger zu bilden. Des Weiteren ist es im Gegensatz zu Pointern nicht möglich, Referenzen zu versetzen. Im Vergleich zu Zeigern können Referenzen nicht 0 sein, sie müssen aber auch nichts referenzieren, es wird nur von Ihnen erwartet! (Da wir ohnehin keine Möglichkeit haben, dies zu überprüfen, lassen wir diese Tatsache einfach links liegen)

    Eine Referenz deklariert man mit ***folgendem Zeichen: &***. D.h. wenn T ein Typ ist, dann ist T& eine Referenz auf T. Referenzen sind zwar syntaktisch einfacher, aber manchmal sind trotzdem Zeiger notwendig, z.B. in Abschnitt 7.

    Hier ein kleines Beispiel mit Referenzen:

    #include <iostream>
    
    using namespace std;
    
    //[kor]Dieses Mal[/kor] erwarten wir eine Referenz auf int als Parameter
    void incrementByRef(int &i) {
      ++i;
    };
    
    //Wir erwarten eine Referenz auf einen Zeiger auf int
    void incrementByRefPtr(int *&i) {
      ++(*i);
    };
    
    int main(int argc, char **argv) {
      int var = 0;
    
      int &ref = var;  //ref ist anderer Name für var (Referenz auf int)
    
      incrementByRef(var);  //var erhöhen
      cout<<ref<<'\n';  //Wird 1 ausgeben
    
      int *p = &var;  //Pointer auf var
      incrementByRefPtr(p);  //p und damit auch var um eins erhöhen
    
      cout<<ref<<'\n';  //Gibt 2 aus
      return 0;
    };
    

    Bei der Rückgabe von Referenzen ist wie bei Zeigern sicherzustellen***,*** dass das referenzierte Objekt nach Verlassen der Funktion noch existiert:

    //Rückgabe einer Referenz auf int
    int& increment() {
      static int var = 0;  //var wird bei jedem Aufruf um eins erhöht
      ++var;
      return var;
    };
    

    Besonderes Augenmerk wird bei Referenzen (für Zeiger siehe Abschnitt 5) auf 'const-correctness' gelegt, d.h. dass übergebene Objekte, die innerhalb einer Funktion nur gelesen werden, als const Referenz deklariert werden. Somit weiß der Aufrufer der Funktion***,*** dass sein Objekt nicht verändert wird.
    Schreiben wir eine Funktion, welche die Integer-Klasse auf die Konsole ausgibt:

    //MyInt wird nur gelesen, deshalb const Referenz
    void printInteger(const Integer &MyInt) {
      cout<<"Integer: "<<MyInt.getData()<<'\n';
    };
    

    Gleiches gilt für die Rückgabe von Referenzen: Darf der Aufrufer der Funktion die zurückgegebene Referenz nur lesen, dann deklariert man eine const-Referenz als Return-Type. Der Aufrufer kann die Konstanz mit const_cast<> zwar wegcasten, aber dass ist dann seine Sache und nicht unser Problem.

    ######################################################################
    4. Zeigerarithmetik
    ######################################################################

    In Verbindung mit Arrays können Zeiger sogar noch mehr, z.B. jedes beliebige Element im Array referenzieren:

    #include <iostream>
    
    using namespace std;
    
    int main(int argc, char **argv) {
      char name[] = "Bruno";
      char *p = name;  //p zeigt auf 'B'
      cout<<p<<'\n' ; //Gibt Bruno aus
    
      ++p;  //p zeigt jetzt auf 'r'
      ++p;  //p zeigt jetzt auf 'u'
      cout<<p<<'\n';  //Gibt uno aus
    
      p = &name[4];  //p zeigt jetzt auf 'o'
      return 0;
    };
    

    Das alles geht, weil der Name eines Arrays ein konstanter Pointer auf das erste Element im Array ist. Deshalb kann man auch "char *p = name;" ("char *p = &name[0];" geht auch) schreiben.
    Wir können den Zeiger auch versetzen***,*** um ihn auf andere Elemente zeigen zu lassen (der Compiler weiß immer***,*** welchen Datentyp ein Array hat und wie groß die Elemente demzufolge sind):

    void printArray(int *array, int elements) {
      for(int i=0;i<elements;++i) {
        //Addiere i*sizeof(int) zum ersten Element im Array und dereferenziere es
        cout<<*(array+i)<<'\n';  
      }
    };
    ...
    int arr[10] = { ... };
    printArray(arr, 10);
    ...
    

    Hier zählt der Compiler immer sizeof(int)i zum ersten Element des Arrays hinzu**,*** um so die anderen zu addressieren - schließlich liegen sie ja hintereinander im Speicher.
    Und um die Verwirrung zu vervollständigen gibt es noch ein kleines Beispiel über die Schreibweisen im Zusammenhang mit Arrays;

    #include <iostream>
    
    using namespace std;
    
    int main(int argc, char **argv) {
      int array[10] = {0,1,2,3,4,5,6,7,8,9};
      int *ptr = array;
    
      cout<<"Adresse von erstem Element: "<<&array[0]<<'\n';
      cout<<"Adresse von letztem Element: "<<(ptr+9)<<'\n';
    
      cout<<"Wert an Index 3: "<<array[3]<<'\n';  //ebenso: *(array+3)
      cout<<"Wert an Index 7: "<<*(ptr+7)<<'\n';  //ebenso: ptr[7]
      return 0;
    };
    

    Wir können die Schreibweisen nach Belieben variieren, allerdings ist es ratsam***,*** bei Arrays die Schreibweise "arr[i]" zu verwenden, und bei Pointern die Schreibweise **"(ptr+i)"***. Alles andere wirkt eher verwirrend, abgesehen von Matrizen, aber dazu später mehr.

    Vorher haben wir bereits gesehen***,*** dass wir Zeiger versetzen können. Wir können sie aber auch subtrahieren***,*** um die Anzahl der Elemente zwischen den Zeigern herauszubekommen, bzw. hier den Index eines Zeigers zu erhalten:

    int array[10] = {0,1,2,3,4,5,6,7,8,9};
    int *p1 = array, *p2 = &array[4];
    
    int num = p2 - p1;
    cout<<num<<endl;  //Gibt 4 aus
    

    Die Addition von Zeigern ist nicht erlaubt***,*** da sie kein sinnvolles Ergebnis zurückliefert. Vergleiche von Zeigern sind aber möglich, strlen könnte man z.B. so implementieren:

    int strlen(char *str) {  //Doesn't count '\0'
      char *p = str;
      for (p = str; *p != '\0'; ++p);  //Zeiger erhöhen bis '\0' gefunden wurde
      return (p-str);
    };
    

    ######################################################################
    5. const bei Pointer
    ######################################################################

    Es gibt einige Möglichkeiten const bei Zeigern anzuwenden, nämlich bei dem Zeiger selber und bei dem Objekt***,*** welches referenziert wird:

    int j = 578;
    
    int *p = &j;  //Variabler Zeiger, variables Objekt
    
    const int *p2 = &j;  //Variabler Zeiger, konstantes Objekt
    //int const *p2 = &j;  //ebenso: Variabler Zeiger, konstantes Objekt
    
    int * const p3 = &j; //Konstanter Zeiger, variables Objekt
    
    const int * const p4 = &j;  //Konstanter Zeiger, konstantes Objekt
    

    Vllt. als Gedankenstütze: Ein const links vom Sternchen bedeutet***,*** dass das***,*** worauf gezeigt wird***,*** konstant ist, rechts davon bedeutet ein konstanter Zeiger.~ab dem letzten Komma: neu machen! Frag mich nicht wie, weil ich es selbst nicht verstehe!~
    Wenn der Zeiger selbst variabel ist, dann können wir ihn auf andere Objekte zeigen lassen***. Ein*** konstanter Zeiger hingegen kann nicht versetzt werden. Wenn das Objekt***,*** welches referenziert wird***,*** konstant ist, dann können wir ihm selbstverständlich nichts zuweisen.

    ######################################################################
    6. Speicher allokieren
    ######################################################################

    Mit Pointern kann man dynamischen Speicher anfordern (und später auch unbedingt wieder freigeben!!). Dies ist z.B. notwendig***,*** wenn man zur Zeit des Kompilierens keine Ahnung hat***,*** wie groß ein Array später werden soll.

    In C wird Speicher mit malloc, calloc oder realloc angefordert und mit free wieder freigegeben.
    In C++ gibt es zwei Operatoren***,*** um Speicher anzufordern (Operator new und Operator new[]) und zwei Operatoren***,*** um den Speicher wieder freizugeben (Operator delete und Operator delete[]). In C übernimmt das immer free.

    WICHTIG: Immer die entsprechenden Operatoren zum Allokieren und Deallokieren verwenden und auch C und C++ nicht mischen: Was mit malloc allokiert wurde, muss mit free wieder freigegeben werden! Was mit new allokiert wurde, muss mit delete wieder freigegeben werden und was mit new [] allokiert wurde, muss mit delete [] wieder freigegeben werden!!
    Und auch wenn man selber keinen Speicher allokiert hat, so kann es sein***,*** dass eine von uns aufgerufene Funktion (z.B. aus einer fremden Bibliothek) Speicher allokiert hat und wir ihn wieder freigeben müssen. In solchen Fällen hilft entweder (falls vorhanden) die Dokumentation, oder (falls vorhanden) der Source-Code weiter. Man sollte dies zum Anlass nehmen und seine eigenen Funktionen ausreichend dokumentieren!

    Zuerst zwei Beispiele mit den C-Funktionen:

    //Einzelnes int dynamisch allokieren:
    //Die casts sind nicht notwendig, aber sie erleichtern das Lesen.
    int *ptr = (int*)malloc(sizeof(int));  //Speicher für einen Integer anfordern
    if (!ptr)  //Pointer gültig? Wenn nicht, abbrechen.
      abort();
    
    //...mit ptr arbeiten
    free(ptr);  //Speicher wieder freigeben.
    ptr = 0;
    
    // Jetzt allokieren wir ein dynamisches Array:
    size_t size;
    cin>>size;
    
    int *array = (int*)malloc(sizeof(int)*size); //Speicher für size ints anfordern
    if (!array)  //Zeiger gültig?
      abort();
    
    //..mit array arbeiten
    free(array);  //Speicher wieder freigeben
    array = 0;
    

    Dann in C++ (Wir benutzen die Integer Klasse von oben):

    Integer *ptr = new Integer(547);  //Speicher für einen Integer anfordern
    //... mit ptr arbeiten
    delete ptr;  //Speicher wieder freigeben.
    ptr = 0;
    
    //Jetzt Integer-Array dynamisch allokieren:
    size_t size;
    cin>>size;
    
    Integer *array=0;
    try {
      //Man kann ja 'size' mal auf 1000000000 setzen und schauen was passiert ;-)
      array = new Integer[size];  //Speicher für size Integer anfordern
      //...mit array arbeiten
    }
    catch(const std::bad_alloc &ba) {  //Evtl. Exception fangen und abbrechen
      cerr<<"Autsch: "<<ba.what()<<'\n';  //Ausgabe der Fehlermeldung
      abort();  //Und raus...
    };
    
    delete [] array;  //Speicher freigeben.
    array = 0;
    

    Wenn malloc (oder seine Geschwister calloc usw.) den dynamischen Speicher nicht kriegen (warum auch immer), dann geben Sie einen so genannten NULL-Zeiger zurück! Deshalb überprüfen wir***,*** ob es ein NULL-Zeiger ist und brechen ab***,*** sofern das zutrifft. Ist natürlich nicht die feine Art, aber hier ist das ok.
    (Im Übrigen kann man auch NULL-Zeiger gefahrlos löschen. Dies wird durch den Standard garantiert.)

    In C++ wird der standarmäßige new Handler aufgerufen und der löst eine Exception aus, wenn kein Speicher allokiert werden konnte (eigentlich ist es ein weniger komplizierter, aber das ist jetzt egal). Darum haben wir hier exemplarisch einen try-catch Block drumherum gebastelt***,*** um eine mögliche Exception abzufangen und uns sauber zurückzuziehen.
    Man kann aber auch bei new das Verhalten von C erzwingen:

    int *ptr = new(nothrow) int;
      if (!ptr)
        abort();
      delete ptr;
    

    Ungeachtet dessen sollte man in C++ deswegen immer (zumindest für Strukturen und Klassen) new und delete verwenden, weil malloc und Konsorten keinen blassen Schimmer von Konstruktoren und Destruktoren haben. D.h. wir reservieren mit malloc zwar Speicher für z.B. ein std::string Objekt, rufen aber keinen Konstruktor auf. Schlecht. Sehr schlecht. (Man kann das Objekt im Nachhinein mittels placement new in den rohen Speicher 'hineinkonstruieren')

    Außerdem ist es sehr***,*** sehr wichtig***,*** den Zeiger auf den dynamisch allokierten Speicher nicht zu 'verlieren', denn wenn das geschieht***,*** hat man ein Ressourcenleck***, und früher oder später wird sich das entweder in der Performance oder mit einem Absturz bemerkbar machen.*** Jedes moderne OS gibt zwar den Speicher nach Beendigung des Programms automatisch frei, aber man sollte sich nicht darauf stützen, sondern den allokierten Speicher selber wieder löschen!
    Nach dem Löschen sollte man dem Pointer 0 zuweisen, denn jetzt referenziert er nichts mehr und ein Zugriff würde zu undefiniertem Verhalten (meistens Absturz des Programms) führen.

    ######################################################################
    7. Pointer auf Pointer
    ######################################################################

    Da Pointer selbst Objekte im Speicher sind, ist es möglich sie von einem Pointer referenzieren zu lassen, also Pointer auf Pointer zu bilden. Die meisten werden auch schon damit in Kontakt gekommen sein; man muss sich nur mal die main() Funktion anschauen:

    int main(int argc, char **argv) {  //Aha!
      return 0;
    };
    

    argv ist in dem Fall eine sogenannte Stringtabelle***,*** welche die Programmargumente enthält. Die einzelnen 'char-Arrays' werden mit argv[0] (Programmname) bis argv[argc-1] angesprochen.

    Im Prinzip funktionieren Pointer auf Pointer wie normale Pointer, aber Code ist hier wohl offensichtlicher:

    #include <iostream>
    
    using namespace std;
    
    int main(int argc, char **argv) {
      int var = 5, var2 = 9;
      int *p = 0;
      p = &var;  //p zeigt auf var
    
      int **pp= 0;
      pp = &p;  //pp zeigt auf p welches auf var zeigt
    
      *p = 715;  //var über p manipulieren
      cout<<"var: "<<var<<'\n';
    
      **pp = 215;  //var über pp manipulieren
      cout<<"var: "<<var<<'\n';
    
      *pp = &var2;  //p zeigt jetzt auf var2, vorher var1
      cout<<"*p: "<<*p<<'\n';
    
      //Schlimmer geht's immer:
      int ***ppp = &pp;
    
      //Um das zu verdeutlichen:
      cout<<"\nPointer und Ihre Werte(p zeigt auf var2 und pp zeigt auf p):\n\n";
    
      cout<<"Adresse von var: "<<&var<<'\n';
      cout<<"Adresse von var2: "<<&var2<<"\n\n";
    
      cout<<"Adresse von p: "<<&p<<'\n';
      cout<<"Wert von p: "<<p<<'\n';
      cout<<"Wert von *p: "<<*p<<"\n\n";
    
      cout<<"Adresse von pp: "<<&pp<<'\n';
      cout<<"Wert von pp: "<<pp<<'\n';
      cout<<"Wert von *pp: "<<*pp<<'\n';
      cout<<"Wert von **pp: "<<**pp<<'\n';
    
      return 0;
    };
    

    Hier sieht man***,*** dass pp die Adresse von p als Wert hat. D.h. also***,*** wir referenzieren mit unserem **int ein *int!
    Das kann man übrigens fast beliebig erweitern, allerdings wird es ab int*** leicht abartig.

    Okay, so weit, so gut, aber wofür kann man das wirklich gebrauchen? Für dynamische 2D Arrays bzw. wenn man ein Array von Pointern dynamisch allokieren will! Doch zuerst etwas Generelles über statische***,*** n-dimensionale Arrays, die man so realisieren kann:

    int arr[2][4];  //2D Array (Matrix) mit zwei Zeilen und vier Spalten
    
    //Hier etwas Murkserei, aber legal, 
    //jedenfalls in C, C++ frisst es nicht
    for (int i=0;i<8;++i)
      arr[0][i] = 0;
    

    Eigentlich haben wir hier ein int Array mit zwei Elementen, wobei jedes der zwei Elemente wiederum ein int-Array der Länge vier enthält. Interessant ist aber***,*** dass die ganze Matrix an einem Stück im Speicher liegt, d.h.***,*** man kann mit dem obigem Code die ganze Matrix auf 0 setzen. Das es Murks ist, brauche ich nicht extra zu sagen, aber es gibt einen guten Einblick***,*** wie die Matrix tatsächlich im Speicher liegt. Möglich wird das auch***,*** da C kein 'run-time range checking' durchführt. C++ hingegen tut es - da klappt dieser Trick (glücklicherweise) nicht mehr.

    Man kann das Spielchen theoretisch bis zu 256 Dimensionen treiben (laut Standard), allerdings dürfte der vorhandene Speicher eher das Limit darstellen.
    Wenn man aber vorher noch nicht weiß***,*** wie groß die Matrix werden soll, dann muss man auf dynamische Zeiger-Arrays zurückgreifen:

    Wieder zuerst in C:

    size_t rows, cols;
    //Irgendwie Werte einlesen, wir nehmen an[kor],[/kor] dass: rows=10 und cols=20
    
    int **arr = 0;  //int Pointer auf int Pointer
    arr = (int**)malloc(sizeof(int*) * rows);  //Zuerst 10 Zeilen allokieren
    
    //In jede der 10 Zeilen ein Array der Länge 20 allokieren
    for (size_t i=0;i<rows;++i)  
      *(arr+i) = (int*)malloc(sizeof(int)*cols);
    
    //... irgendwas mit arr machen
    
    for (size_t i=0;i<rows;++i)  //Zuerst Spalten freigeben
      free( *(arr+i) );
    
    free(arr);  //Zeilen freigeben
    arr = 0;
    

    Selbiges in C++:

    size_t rows, cols;
    //Irgendwie Werte einlesen
    
    int **arr = 0;
    arr = new int*[rows];  //Zeilen allokieren
    
    for (size_t i=0;i<rows;++i)
      *(arr+i) = new int[cols];  //Spalten allokieren
    
    //... irgendwas mit arr machen
    
    for (size_t i=0;i<rows;++i)
      delete [] *(arr+i);  //Spalten freigeben
    
    delete [] arr;  //Zeilen freigeben
    arr = 0;
    

    Diese schicken Konstrukte kann man natürlich auch an Funktionen übergeben oder sich zurückgeben lassen:

    #include <iostream>
    
    using namespace std;
    
    void printMatrix(int **mat, size_t rows, size_t cols) {
      for (size_t i=0;i<rows;++i) {
        cout<<"Row: "<<i<<'\n';
        for (size_t j=0;j<cols;++j) {
          cout<<"\tColumn: "<<j<<" : ";
          cout<<mat[i][j]<<'\n';
        }
      }
    };
    
    // Erstellt eine Matrix mit Operator new []
    int **createMatrix(size_t rows, size_t cols) {
      int **arr = 0;
      arr = new int*[rows];
    
      for (size_t i=0;i<rows;++i)
        *(arr+i) = new int[cols];
    
      return arr;
    };
    
    // Löscht eine Matrix mit Operator delete []
    void killMatrix(int **mat, size_t rows, size_t cols) {
      for (size_t i=0;i<rows;++i)
        delete [] *(mat+i);
    
      delete [] mat;
      mat = 0;
    };
    
    int main(int argc, char **argv) {
      size_t rows=10, cols=20;
    
      int **arr = createMatrix(rows, cols);
    
      //Irgendwelche schräge Werte setzen:
      for (int i=0;i<rows;++i)
        for (int j=0;j<cols;++j)
          arr[i][j] = i+j;
    
      printMatrix(arr, rows, cols);
    
      //Löschen nicht vergessen, 
      //Speicher wurde in createMatrix mit new[] allokiert!!
      killMatrix(arr, rows, cols);
      return 0;
    };
    

    Eigentlich arbeiten wir hier mit Pointern***;*** weshalb also behandeln wir sie wie Arrays***,*** wo doch oben steht***,*** man solle auf die entsprechende Schreibweise achten? Na ja, meiner Meinung nach vereinfacht die Arrayschreibweise das ganze ungemein, man kann es einfach besser lesen. Nehmen wir mal an***,*** wir haben eine Matrix wie oben bereits mit Werten belegt:

    //Gibt alles das gleiche aus:
    cout<<arr[5][7]<<'\n';  //Schön 
    cout<<*(arr[5]+7)<<'\n';  //Grausam
    cout<<(*(arr+5))[7]<<'\n';  //Auch grausam
    cout<<*(*(arr+5)+7)<<'\n';  //Igitt
    

    Zum Abschluss gibt es noch ein Listing mit dynamischen 3D Arrays. Wie bei den Matrizen arbeiten wir uns beim Erstellen von außen nach innen vor, beim Löschen wird umgekehrt vorgegangen:

    #include <iostream>
    
    using namespace std;
    
    template<typename T>  //3D-Array ausgeben
    void print3D(T **mat, size_t x, size_t y, size_t z) {
      for (size_t i=0;i<x;++i)
        for (size_t j=0;j<y;++j)
          for (size_t k=0;k<z;++k)
    	cout<< *(*(*(mat+i)+j)+k) <<'\n';
    };
    
    template <typename T>  //3D-Array erstellen
    T ***create3D(size_t x, size_t y, size_t z) {
      //Im Prinzip haben wir hier nur ein Array(T***) mit T** Elementen wobei 
      //jedes dieser T** ein weiteres Array von T* enthält.
      //Vllt. geht das auch einfacher???
      T ***arr=0;
    
      for (size_t i=0;i<x;++i) 
        arr = new T**[x];
    
      for (size_t i=0;i<y;++i)
        *(arr+i) = new T*[y];
    
      for (size_t i=0;i<y;i++)
        for (size_t j=0;j<z;++j) 
         *(*(arr+i)+j) = new T[z];
    
      return arr;
    };
    
    template <typename T>  //3D-Array wieder löschen
    void kill3D(T ***mat, size_t x, size_t y, size_t z) {
      for (size_t i=0;i<y;i++)
        for (size_t j=0;j<z;++j) 
          delete [] *(*(mat+i)+j);
    
      for (size_t i=0;i<y;++i)
        delete [] *(mat+i);
    
      delete [] mat;
      mat = 0;
    };
    
    int main(int argc, char **argv) {
      size_t x=10,y=10,z=10;
    
      int ***mat = create3D<int>(x,y,z);
    
      for (size_t i=0;i<x;++i)
        for (size_t j=0;j<y;++j)
          for (size_t k=0;k<z;++k)
    	mat[i][j][k] = i+j+k;
    
      print3D(mat,x,y,z);
    
      kill3D(mat,x,y,z);
    
      return 0;
    };
    

    Hoffentlich wurde etwas Klarheit in dieses Sternchenminenfeld gebracht. Wenn nicht, kann man ja nachfragen.

    =========================================================================================

    Eigentlich ziemlich fehlerfrei, nur eine Sache:
    Vor Nebensätze setzt man Kommata und Hauptsätze trennt man voneinander mit Punkten.

    Also vor "dass" und "um" und bei "wie", "wo", "was", "der", "die", "das", die Nebensätze (Relativsätze) einleiten, Komma setzen!

    Mr. B



  • scheiße, war das lang, du. ich hab daran mind. ne stunde und n bissl dran gesessen.
    Mein Appell an euch: Schreibt nicht so lange Artikel! 😉

    Mr. B



  • Mr. B schrieb:

    scheiße, war das lang, du. ich hab daran mind. ne stunde und n bissl dran gesessen.
    Mein Appell an euch: Schreibt nicht so lange Artikel! 😉

    Mr. B

    Erstmal fettes DANKE für die Korrektur. Werd's über's nächste Wochenende angehen. Früher geht nicht.
    Und ich kann dich beruhigen, der nächste wird kürzer, aber dieses Thema war irgendwie recht umfangreich...



  • Dieser Artikel wird sich mit Zeigern (engl. Pointer) in C bzw. C++ beschäftigen. Der Quellcode wurde mit dem g++ 3.3.6 getestet. Sollten sich trotzdem irgendwelche groben Fehler eingeschlichen haben, bitte ich um Mitteilung.

    Folgendes wird behandelt:

    1. Einführung
    2. Zugriff auf Daten durch Pointer
    3. Übergabe/Rückgabe von Pointern
    3.1. Exkurs Referenzen
    4. Zeigerarithmetik
    5. const bei Pointern
    6. Speicher allokieren
    7. Pointer auf Pointer

    ######################################################################
    1. Einführung
    ######################################################################

    Viele verteufeln sie als fehleranfällig und kompliziert, aber wir brauchen sie trotzdem: Pointer, z.B. um Datenstrukturen wie Listen oder Bäume effizient zu implementieren oder einfach um das Kopieren eines Objekts bei einer Übergabe zu vermeiden.
    Für Einsteiger ist das Durcheinander der Sternchen und &-Zeichen meistens recht verwirrend. Mal sehen, ob wir etwas Klarheit reinbringen können...

    Zuerst werden wir uns anhand eines kurzen Beispiels anschauen, was normale Variablen und Pointervariablen im Speicherbild unterscheidet:

    #include <iostream>
    
    using namespace std;
    
    int main(int argc, char **argv) {
      int i = 5;  //int Variable
      int *p = 0;  //Pointer auf int, mit 0 initialisert!
      p = &i;  //mit Operator & Adresse von i holen und an p zuweisen
      cout<<"Adresse von i: "<<&i<<'\n';
      cout<<"Wert von p: "<<p<<'\n';
      cout<<"Adresse von p: "<<&p<<'\n';
      return 0;
    };
    

    Bei mir ergeben sich folgende Werte: Variable i hat den Wert 5 und die Adresse 0xbffff514, Pointer-Variable p hat den Wert 0xbffff514 (Adresse von i) und selbst die Adresse 0xbffff510!

    Vereinfacht gesagt ist ein Pointer auch nur eine Variable im Hauptspeicher, die eben eine Adresse als Wert hat.

    Im obigen Beispiel haben wir gesehen, dass man einen Zeiger auf int mit dem *-Zeichen deklariert und an die Adresse eines Objekts mit dem &-Operator kommt. Übrigens sollte ein Zeiger auf int auch nur auf ints zeigen und nicht auf andere Typen wie double!
    Allerdings haben wir dem Zeiger zuerst die Adresse 0 zugewiesen. Das haben wir getan, weil die 0 bei Zeigern eine ganz spezielle Bedeutung hat, nämlich dass der Zeiger momentan ungültig ist bzw. auf nichts Gültiges zeigt (der Standard garantiert, dass gültige Adressen ungleich 0 sind). D.h. man kann/sollte einen Zeiger vor Benutzung auf 0 prüfen, um ein Segmentation Fault zu vermeiden.

    Wenn ein Zeiger bei der Deklaration noch nichts referenzieren kann oder soll, dann ist es sehr, sehr empfehlenswert, ihn vorbeugend mit 0 zu initialisieren.
    Man kann anstatt 0 auch die symbolische Konstante NULL verwenden, das ist gleichwertig und kann auf den ersten Blick offensichtlicher machen, dass mit Zeigern gearbeitet wird. Ob man jetzt 0 oder NULL schreibt, bleibt jedem selbst überlassen. Für den Artikel werde ich 0 verwenden.

    ######################################################################
    2. Zugriff auf Daten durch Pointer
    ######################################################################

    Wenn der Wert eines Zeiger die Adresse einer Variablen ist, dann muss man auch an den Wert der Variablen selbst herankommen. Das Ganze nennt sich Derefenzieren und geschieht mit dem *-Operator:

    #include <iostream>
    
    using namespace std;
    
    int main(int argc, char **argv) {
      int i = 5, j;
      int *p = &i;	//mit Operator & Adresse von i holen und an p zuweisen
      j = *p;  //Derefenziere p mit Operator * und weise Ergebnis j zu
      cout<<j<<'\n';  //Gibt 5 aus
    
      p = &j;  //p zeigt jetzt auf j
      *p = 215;  //p dereferenzieren und 215 zuweisen
      cout<<j<<'\n';  //Gibt 215 aus
      return 0;
    };
    

    Hier sieht man, dass p nur einen Verweis auf i (und dann auf j) darstellt. Wenn man also p dereferenziert und ändert, dann ändert man auch die Variable, auf die p zeigt.

    Strukturen und Klassen besitzen meistens Datenelemente. Da es möglich ist, Zeiger auf Strukturen zu bilden, muss es auch möglich sein, über den Zeiger auf die Datenelemente zuzugreifen, und zwar so:

    #include <iostream>
    
    using namespace std;
    
    //Billig-Wrapper für ints, müssten natürlich noch Operatoren +, += usw. dazu
    class Integer {
    public:
      explicit Integer(int i=0) : data(i) {}
      Integer(const Integer &i) : data(i.data) {}
      ~Integer() {}
    
      inline void setData(int i) { data = i; }
      inline int getData() const { return data; }
    
      Integer& operator=(int i) {
        data = i;
        return *this;
      }
    
      Integer& operator=(const Integer &i) {
        if (this == &i)
          return *this;
    
        data = i.data;
        return *this;
      }
    private:
      int data;
      operator int () const;
    };
    
    int main(int argc, char **argv) {
      Integer myInt(7);
    
      Integer *pi = &myInt;
      (*pi).setData(12);
      cout<<pi->getData()<<'\n';  //Wir benutzen den Pfeil-Operator
      return 0;
    };
    

    Prinzipiell ist es egal, ob man die erste oder zweite Schreibweise wählt, aber die zweite ist doch wesentlich komfortabler und offenbart gleich, dass man mit einem Zeiger arbeitet.

    Mit einem Zeiger auf ein Objekt kann man ein nettes Spielchen spielen, das da heißt: Verstecke ein Objekt hinter einem Zeiger! Und so spielt man es: Zeiger sind unabhängig vom Typ, auf den sie zeigen, auf 32 Bit-Rechnern immer 4 Byte (Speicherung einer Adresse braucht dort so viel Platz) groß. Wenn ich jetzt in meiner Klasse X die Membervariable Integer myInt habe, dann wird beim Kompilieren und einer vorangegangenen Veränderung von Integer X ebenfalls neu kompiliert, obwohl sich an X eigentlich nichts verändert hat. Wenn ich aber die Membervariable zu einem Zeiger auf Integer mache, dann wird X nicht neu kompiliert, denn an X selbst ändert sich nichts, egal was mit Integer passiert. Nett, oder?

    ######################################################################
    3. Übergabe/Rückgabe von Pointern
    ######################################################################

    Funktionen können Parameter haben, d.h. wir übergeben diesen Funktionen Daten, mit denen sie dann arbeiten. Das tun wir entweder 'by Value' oder 'by Reference':

    #include <iostream>
    
    using namespace std;
    
    //Übergabe by Value, int Variable wird beim Funktionsaufruf kopiert
    int incrementByVal(int i) {
      return ++i;
    };
    
    //Übergabe by Reference, wir arbeiten direkt mit der int Variable
    void incrementByRef(int *p) {
      ++(*p);  //Dereferenzieren und erhöhen, wir erhöhen nicht den Zeiger selbst!
    };
    
    int main(int argc, char **argv) {
      int j=5;
      cout<<"j (by Value): "<<incrementByVal(j)<<'\n';  //Gibt 6 aus
    
      cout<<"j(vorher, by Ref): "<<j<<'\n';  //Gibt 5 aus
    
      //Da die Funktion einen Pointer auf int erwartet, übergeben wir eine Adresse!
      incrementByRef(&j);  
      cout<<"j(nachher, by Ref): "<<j<<'\n';  //Gibt 6 aus
    
      return 0;
    };
    

    Bei der Übergabe by Value erhalten wir eine Kopie der int-Variable und müssen dann das Ergebnis mittels return zurückgeben, d.h. wir zahlen dafür, dass die int-Variable kopiert werden muss. Gut, bei int ist es nicht viel, aber bei größeren, komplexeren Datenstrukturen (std::string ist schon zu groß) rechnet sich das auf alle Fälle.
    Bei der Übergabe by Reference arbeiten wir direkt mit dem Objekt; es findet kein Kopieren und kein return statt. Wir zahlen gar nichts fürs Kopieren, fast nichts, denn jetzt können wir keine Ausdrücke wie j+5 mehr übergeben. Dieser Ausdruck hat nämlich keine Adresse im Hauptspeicher und kann somit von einem Pointer nicht referenziert werden. Außerdem braucht natürlich der Pointer selbst 4 Bytes.

    Natürlich kann man Zeiger auch als Return-Werte einsetzen, allerdings muss man unbedingt sicherstellen, dass die Variable, auf die der Zeiger verweist, auch nach dem Verlassen der Funktion noch existiert, sonst wird unser Programm beim Zugriff auf den Zeiger abschmieren:

    int * increment(int i) {
      int var = i;
      ++var;
      return &var;
    };  //hier wird var zerstört, wohin zeigt der Pointer jetzt??
    

    Wenn man jetzt versucht den Zeiger zu dereferenzieren, wird das Programm mit Sicherheit (irgendwann) mit einem Segmentation Fault abstürzen!

    Eine Lösung wäre var static zu deklarieren, um sicherzustellen, dass die Variable auch nach Verlassen der Funktion noch existiert:

    #include <iostream>
    
    using namespace std;
    
    int * increment(int i) {
      static int var = i;  //var ist jetzt static, bleibt also erhalten
      ++var;
      return &var;
    };
    
    int main(int argc, char **argv) {
      int j=5;
      int *p = increment(j);
      cout<<*p<<'\n';  //Gibt 6 aus
      cout<<j<<'\n';  //Gibt 5 aus
      return 0;
    };
    

    ######################################################################
    3.1. Exkurs Referenzen
    ######################################################################

    Im Zusammenhang mit der by Reference Übergabe möchte ich kurz Referenzen (nur C++) ansprechen, die den Zeigern zwar ähneln, aber keine sind. Der wichtigste Unterschied ist wohl, dass Referenzen immer nur einen anderen Namen für das referenzierte Objekt darstellen und somit keinen eigenen Speicher belegen. Dass wiederum bedeutet, dass es nicht möglich ist, Zeiger auf Referenzen, wohl aber Referenzen auf Zeiger zu bilden. Des Weiteren ist es im Gegensatz zu Pointern nicht möglich, Referenzen zu versetzen. Im Vergleich zu Zeigern können Referenzen nicht 0 sein, sie müssen aber auch nichts referenzieren, es wird nur von Ihnen erwartet! (Da wir ohnehin keine Möglichkeit haben, dies zu überprüfen, lassen wir diese Tatsache einfach links liegen)

    Eine Referenz deklariert man mit folgendem Zeichen: &. D.h. wenn T ein Typ ist, dann ist T& eine Referenz auf T. Referenzen sind zwar syntaktisch einfacher, aber manchmal sind trotzdem Zeiger notwendig, z.B. in Abschnitt 7.

    Hier ein kleines Beispiel mit Referenzen:

    #include <iostream>
    
    using namespace std;
    
    //Diesesmal erwarten wir eine Referenz auf int als Parameter
    void incrementByRef(int &i) {
      ++i;
    };
    
    //Wir erwarten eine Referenz auf einen Zeiger auf int
    void incrementByRefPtr(int *&i) {
      ++(*i);
    };
    
    int main(int argc, char **argv) {
      int var = 0;
    
      int &ref = var;  //ref ist anderer Name für var (Referenz auf int)
    
      incrementByRef(var);  //var erhöhen
      cout<<ref<<'\n';  //Wird 1 ausgeben
    
      int *p = &var;  //Pointer auf var
      incrementByRefPtr(p);  //p und damit auch var um eins erhöhen
    
      cout<<ref<<'\n';  //Gibt 2 aus
      return 0;
    };
    

    Bei der Rückgabe von Referenzen ist wie bei Zeigern sicherzustellen, dass das referenzierte Objekt nach Verlassen der Funktion noch existiert:

    //Rückgabe einer Referenz auf int
    int& increment() {
      static int var = 0;  //var wird bei jedem Aufruf um eins erhöht
      ++var;
      return var;
    };
    

    Besonderes Augenmerk wird bei Referenzen(für Zeiger siehe Abschnitt 5) auf 'const-correctness' gelegt, d.h. dass übergebene Objekte, die innerhalb einer Funktion nur gelesen werden, als const Referenz deklariert werden. Somit weiß der Aufrufer der Funktion, dass sein Objekt nicht verändert wird.
    Schreiben wir eine Funktion, welche die Integer-Klasse auf die Konsole ausgibt:

    //MyInt wird nur gelesen, deshalb const Referenz
    void printInteger(const Integer &MyInt) {
      cout<<"Integer: "<<MyInt.getData()<<'\n';
    };
    

    Gleiches gilt für die Rückgabe von Referenzen: Darf der Aufrufer der Funktion die zurückgegebene Referenz nur lesen, dann deklariert man eine const-Referenz als Return-Type. Der Aufrufer kann die Konstanz mit const_cast<> zwar wegcasten, aber dass ist dann seine Sache und nicht unser Problem.

    ######################################################################
    4. Zeigerarithmetik
    ######################################################################

    In Verbindung mit Arrays können Zeiger sogar noch mehr, z.B. jedes beliebige Element im Array referenzieren:

    #include <iostream>
    
    using namespace std;
    
    int main(int argc, char **argv) {
      char name[] = "Bruno";
      char *p = name;  //p zeigt auf 'B'
      cout<<p<<'\n' ; //Gibt Bruno aus
    
      ++p;  //p zeigt jetzt auf 'r'
      ++p;  //p zeigt jetzt auf 'u'
      cout<<p<<'\n';  //Gibt uno aus
    
      p = &name[4];  //p zeigt jetzt auf 'o'
      return 0;
    };
    

    Das alles geht, weil der Name eines Arrays ein konstanter Pointer auf das erste Element im Array ist. Deshalb kann man auch "char *p = name;" ("char *p = &name[0];" geht auch) schreiben.
    Wir können den Zeiger auch versetzen um ihn auf andere Elemente zeigen zu lassen (der Compiler weiß immer, welchen Datentyp ein Array hat und wie groß die Elemente demzufolge sind):

    void printArray(int *array, int elements) {
      for(int i=0;i<elements;++i) {
        //Addiere i*sizeof(int) zum ersten Element im Array und dereferenziere es
        cout<<*(array+i)<<'\n';  
      }
    };
    ...
    int arr[10] = { ... };
    printArray(arr, 10);
    ...
    

    Hier zählt der Compiler immer sizeof(int)*i zum ersten Element des Arrays hinzu, um so die anderen zu addressieren, schließlich liegen sie ja hintereinander im Speicher.
    Und um die Verwirrung zu vervollständigen gibt es noch ein kleines Beispiel über die Schreibweisen im Zusammenhang mit Arrays:

    #include <iostream>
    
    using namespace std;
    
    int main(int argc, char **argv) {
      int array[10] = {0,1,2,3,4,5,6,7,8,9};
      int *ptr = array;
    
      cout<<"Adresse von erstem Element: "<<&array[0]<<'\n';
      cout<<"Adresse von letztem Element: "<<(ptr+9)<<'\n';
    
      cout<<"Wert an Index 3: "<<array[3]<<'\n';  //ebenso: *(array+3)
      cout<<"Wert an Index 7: "<<*(ptr+7)<<'\n';  //ebenso: ptr[7]
      return 0;
    };
    

    Wir können die Schreibweisen nach Belieben variieren, allerdings ist es ratsam, bei Arrays die "arr[i]" Schreibweise zu verwenden, und bei Pointern die "*(ptr+i)" Schreibweise. Alles andere wirkt eher verwirrend, abgesehen von Matrizen, aber dazu später mehr.

    Vorher haben wir bereits gesehen, dass wir Zeiger versetzen können, wir können sie aber auch subtrahieren, um die Anzahl der Elemente zwischen den Zeigern herauszubekommen, bzw. hier den Index eines Zeigers zu erhalten:

    int array[10] = {0,1,2,3,4,5,6,7,8,9};
    int *p1 = array, *p2 = &array[4];
    
    int num = p2 - p1;
    cout<<num<<endl;  //Gibt 4 aus
    

    Die Addition von Zeigern ist nicht erlaubt, da sie kein sinnvolles Ergebnis zurückliefert, Vergleiche von Zeigern sind aber möglich, strlen könnte man z.B. so implementieren:

    int strlen(char *str) {  //Doesn't count '\0'
      char *p = str;
      for (p = str; *p != '\0'; ++p);  //Zeiger erhöhen bis '\0' gefunden wurde
      return (p-str);
    };
    

    ######################################################################
    5. const bei Pointer
    ######################################################################

    Es gibt einige Möglichkeiten const bei Zeigern anzuwenden, nämlich bei dem Zeiger selber und bei dem Objekt, welches referenziert wird:

    int j = 578;
    
    int *p = &j;  //Variabler Zeiger, variables Objekt
    
    const int *p2 = &j;  //Variabler Zeiger, konstantes Objekt
    //int const *p2 = &j;  //ebenso: Variabler Zeiger, konstantes Objekt
    
    int * const p3 = &j; //Konstanter Zeiger, variables Objekt
    
    const int * const p4 = &j;  //Konstanter Zeiger, konstantes Objekt
    

    Vllt. als Gedankenstütze: Ein const links vom Sternchen bedeutet, dass das, worauf gezeigt wird, konstant ist, rechts davon bedeutet, dass der Zeiger selbst konstant ist.
    Wenn der Zeiger selbst variabel ist, dann können wir ihn auf andere Objekte zeigen lassen. Ein konstanter Zeiger hingegen kann nicht versetzt werden. Wenn das Objekt, welches referenziert, wird konstant ist, dann können wir ihm selbstverständlich nichts zuweisen.

    ######################################################################
    6. Speicher allokieren
    ######################################################################

    Mit Pointern kann man dynamischen Speicher anfordern (und später auch unbedingt wieder freigeben!!). Dies ist z.B. notwendig, wenn man zur Zeit des Kompilierens keine Ahnung hat, wie groß ein Array später werden soll.

    In C wird Speicher mit malloc, calloc oder realloc angefordert und mit free wieder freigegeben.
    In C++ gibt es zwei Operatoren, um Speicher anzufordern (Operator new und Operator new[]) und zwei Operatoren, um den Speicher wieder freizugeben (Operator delete und Operator delete[]). In C übernimmt das immer free.

    WICHTIG: Immer die entsprechenden Operatoren zum Allokieren und Deallokieren verwenden und auch C und C++ nicht mischen: Was mit malloc allokiert wurde, muss mit free wieder freigegeben werden! Was mit new allokiert wurde, muss mit delete wieder freigegeben werden und was mit new [] allokiert wurde, muss mit delete [] wieder freigegeben werden!!
    Und auch wenn man selber keinen Speicher allokiert hat, so kann es sein, dass eine von uns aufgerufene Funktion (z.B. aus einer fremden Bibliothek) Speicher allokiert hat und wir ihn wieder freigeben müssen. In solchen Fällen hilft entweder (falls vorhanden) die Dokumentation, oder (falls vorhanden) der Source-Code weiter. Man sollte dies zum Anlass nehmen und seine eigenen Funktionen ausreichend dokumentieren!

    Zuerst zwei Beispiele mit den C-Funktionen:

    //Einzelnes int dynamisch allokieren:
    //Die casts sind nicht notwendig, aber sie erleichtern das Lesen.
    int *ptr = (int*)malloc(sizeof(int));  //Speicher für ein int anfordern
    if (!ptr)  //Pointer gültig? Wenn nicht, abbrechen.
      abort();
    
    //...mit ptr arbeiten
    free(ptr);  //Speicher wieder freigeben.
    ptr = 0;
    
    // Jetzt allokieren wir ein dynamisches Array:
    size_t size;
    cin>>size;
    
    int *array = (int*)malloc(sizeof(int)*size); //Speicher für size ints anfordern
    if (!array)  //Zeiger gültig?
      abort();
    
    //..mit array arbeiten
    free(array);  //Speicher wieder freigeben
    array = 0;
    

    Dann in C++ (Wir benutzen die Integer Klasse von oben):

    Integer *ptr = new Integer(547);  //Speicher für ein Integer anfordern
    //... mit ptr arbeiten
    delete ptr;  //Speicher wieder freigeben.
    ptr = 0;
    
    //Jetzt Integer-Array dynamisch allokieren:
    size_t size;
    cin>>size;
    
    Integer *array=0;
    try {
      //Man kann ja 'size' mal auf 1000000000 setzen und schauen was passiert ;-)
      array = new Integer[size];  //Speicher für size Integer anfordern
      //...mit array arbeiten
    }
    catch(const std::bad_alloc &ba) {  //Evtl. Exception fangen und abbrechen
      cerr<<"Autsch: "<<ba.what()<<'\n';  //Ausgabe der Fehlermeldung
      abort();  //Und raus...
    };
    
    delete [] array;  //Speicher freigeben.
    array = 0;
    

    Wenn malloc (oder seine Geschwister calloc usw.) den dynamischen Speicher nicht kriegen (warum auch immer), dann geben Sie einen so genannten NULL-Zeiger zurück! Deshalb überprüfen wir, ob es ein NULL-Zeiger ist und brechen ab, sofern das zutrifft. Ist natürlich nicht die feine Art, aber hier ist das ok.
    (Im Übrigen kann man auch NULL-Zeiger gefahrlos löschen, dies wird durch den Standard garantiert)

    In C++ wird der standarmäßige new Handler aufgerufen und der löst eine Exception aus, wenn kein Speicher allokiert werden konnte (Eigentlich ist es ein weniger komplizierter, aber das ist jetzt egal). Darum haben wir hier exemplarisch einen try-catch Block drumherum gebastelt, um eine mögliche Exception abzufangen und uns sauber zurückzuziehen.
    Man kann aber auch bei new das Verhalten von C erzwingen:

    int *ptr = new(nothrow) int;
      if (!ptr)
        abort();
      delete ptr;
    

    Ungeachtet dessen sollte man in C++ deswegen immer (zumindest für Strukturen und Klassen) new und delete verwenden, und zwar weil malloc und Konsorten keinen blassen Schimmer von Konstruktoren/Destruktoren haben, d.h. wir reservieren mit malloc zwar Speicher für z.B. ein std::string Objekt, rufen aber keinen Konstruktor auf. Schlecht. Sehr schlecht. (Man kann das Objekt im Nachhinein mittels placement new in den rohen Speicher 'hinein-konstruieren')

    Außerdem ist es sehr, sehr wichtig den Zeiger auf den dynamisch allokierten Speicher nicht zu 'verlieren', denn wenn das geschieht, hat man ein Ressourcenleck, und früher oder später wird sich das entweder in der Performance oder mit einem Absturz bemerkbar machen. Jedes moderne OS gibt zwar den Speicher nach Beendigung des Programms automatisch frei, aber man sollte sich nicht darauf stützen, sondern den allokierten Speicher selber wieder löschen!
    Nach dem Löschen sollte man dem Pointer 0 zuweisen, denn jetzt referenziert er nichts mehr und ein Zugriff würde zu undefiniertem Verhalten (meistens Absturz des Programms) führen.

    ######################################################################
    7. Pointer auf Pointer
    ######################################################################

    Da Pointer selbst Objekte im Speicher sind, ist es möglich sie von einem Pointer referenzieren zu lassen, also Pointer auf Pointer zu bilden. Die meisten werden auch schon damit in Kontakt gekommen sein, man muss sich nur mal die main() Funktion anschauen:

    int main(int argc, char **argv) {  //Aha!
      return 0;
    };
    

    argv ist in dem Fall eine sogenannte Stringtabelle welche die Programmargumente enthält. Die einzelnen 'char-Arrays' werden mit argv[0] (Programmname) bis argv[argc-1] angesprochen.

    Im Prinzip funktionieren Pointer auf Pointer wie normale Pointer, aber Code ist hier wohl offensichtlicher:

    #include <iostream>
    
    using namespace std;
    
    int main(int argc, char **argv) {
      int var = 5, var2 = 9;
      int *p = 0;
      p = &var;  //p zeigt auf var
    
      int **pp= 0;
      pp = &p;  //pp zeigt auf p welches auf var zeigt
    
      *p = 715;  //var über p manipulieren
      cout<<"var: "<<var<<'\n';
    
      **pp = 215;  //var über pp manipulieren
      cout<<"var: "<<var<<'\n';
    
      *pp = &var2;  //p zeigt jetzt auf var2, vorher var1
      cout<<"*p: "<<*p<<'\n';
    
      //Schlimmer geht's immer:
      int ***ppp = &pp;
    
      //Um das zu verdeutlichen:
      cout<<"\nPointer und Ihre Werte(p zeigt auf var2 und pp zeigt auf p):\n\n";
    
      cout<<"Adresse von var: "<<&var<<'\n';
      cout<<"Adresse von var2: "<<&var2<<"\n\n";
    
      cout<<"Adresse von p: "<<&p<<'\n';
      cout<<"Wert von p: "<<p<<'\n';
      cout<<"Wert von *p: "<<*p<<"\n\n";
    
      cout<<"Adresse von pp: "<<&pp<<'\n';
      cout<<"Wert von pp: "<<pp<<'\n';
      cout<<"Wert von *pp: "<<*pp<<'\n';
      cout<<"Wert von **pp: "<<**pp<<'\n';
    
      return 0;
    };
    

    Hier sieht man, dass pp die Adresse von p als Wert hat. D.h. also, wir referenziern mit unserem **int ein *int!
    Das kann man übrigens fast beliebig erweitern, allerdings wird's ab int*** leicht abartig.

    Okay, so weit, so gut, aber wofür kann man das wirklich gebrauchen? Für dynamische 2D Arrays, bzw. wenn man ein Array von Pointern dynamisch allokieren will! Doch zuerst etwas Generelles über statische, n-dimensionale Arrays, die man so realisieren kann:

    int arr[2][4];  //2D Array (Matrix) mit 2 Zeilen und 4 Spalten
    
    //Hier etwas murkserei, aber legal, 
    //jedenfalls in C, C++ frisst es nicht
    for (int i=0;i<8;++i)
      arr[0][i] = 0;
    

    Eigentlich haben wir hier ein int Array mit 2 Elementen, wobei jedes der 2 Elemente wiederum ein int-Array der Länge 4 'enthält'. Interessant ist aber, dass die ganze Matrix an einem Stück im Speicher liegt, d.h., man kann mit dem obigen Code die ganze Matrix auf 0 setzen. Das es Murks ist, brauche ich nicht extra zu sagen, aber es gibt einen guten Einblick wie die Matrix tatsächlich im Speicher liegt. Möglich wird das auch, da C kein 'run-time range checking' durchführt. C++ hingegen tut es, da klappt dieser Trick (glücklicherweise) nicht mehr.

    Man kann das Spielchen theoretisch bis zu 256 Dimensionen treiben (laut Standard), allerdings dürfte der vorhandene Speicher eher das Limit darstellen.
    Wenn man aber vorher noch nicht weiß, wie groß die Matrix werden soll, dann muss man auf dynamische Zeiger-Arrays zurückgreifen:

    Wieder zuerst in C:

    size_t rows, cols;
    //Irgendwie Werte einlesen, wir nehmen an dass: rows=10 und cols=20
    
    int **arr = 0;  //int Pointer auf int Pointer
    arr = (int**)malloc(sizeof(int*) * rows);  //Zuerst 10 Zeilen allokieren
    
    //In jede der 10 Zeilen ein Array der Länge 20 allokieren
    for (size_t i=0;i<rows;++i)  
      *(arr+i) = (int*)malloc(sizeof(int)*cols);
    
    //... irgendwas mit arr machen
    
    for (size_t i=0;i<rows;++i)  //Zuerst Spalten freigeben
      free( *(arr+i) );
    
    free(arr);  //Zeilen freigeben
    arr = 0;
    

    Selbiges in C++:

    size_t rows, cols;
    //Irgendwie Werte einlesen
    
    int **arr = 0;
    arr = new int*[rows];  //Zeilen allokieren
    
    for (size_t i=0;i<rows;++i)
      *(arr+i) = new int[cols];  //Spalten allokieren
    
    //... irgendwas mit arr machen
    
    for (size_t i=0;i<rows;++i)
      delete [] *(arr+i);  //Spalten freigeben
    
    delete [] arr;  //Zeilen freigeben
    arr = 0;
    

    Diese schicken Konstrukte kann man natürlich auch an Funktionen übergeben oder sich zurückgeben lassen:

    #include <iostream>
    
    using namespace std;
    
    void printMatrix(int **mat, size_t rows, size_t cols) {
      for (size_t i=0;i<rows;++i) {
        cout<<"Row: "<<i<<'\n';
        for (size_t j=0;j<cols;++j) {
          cout<<"\tColumn: "<<j<<" : ";
          cout<<mat[i][j]<<'\n';
        }
      }
    };
    
    // Erstellt eine Matrix mit Operator new []
    int **createMatrix(size_t rows, size_t cols) {
      int **arr = 0;
      arr = new int*[rows];
    
      for (size_t i=0;i<rows;++i)
        *(arr+i) = new int[cols];
    
      return arr;
    };
    
    // Löscht eine Matrix mit Operator delete []
    void killMatrix(int **mat, size_t rows, size_t cols) {
      for (size_t i=0;i<rows;++i)
        delete [] *(mat+i);
    
      delete [] mat;
      mat = 0;
    };
    
    int main(int argc, char **argv) {
      size_t rows=10, cols=20;
    
      int **arr = createMatrix(rows, cols);
    
      //Irgendwelche schrägen Werte setzen:
      for (int i=0;i<rows;++i)
        for (int j=0;j<cols;++j)
          arr[i][j] = i+j;
    
      printMatrix(arr, rows, cols);
    
      //Löschen nicht vergessen, 
      //Speicher wurde in createMatrix mit new[] allokiert!!
      killMatrix(arr, rows, cols);
      return 0;
    };
    

    Eigentlich arbeiten wir hier mit Pointern; also weshalb behandeln wir sie wie Arrays, wo doch oben steht, man solle auf die entsprechende Schreibweise achten? Na ja, meiner Meinung nach vereinfacht die Arrayschreibweise das ganze ungemein, man kann's einfach besser lesen. Nehmen wir mal an, wir haben eine Matrix wie oben bereits mit Werten belegt:

    //Gibt alles das gleiche aus:
    cout<<arr[5][7]<<'\n';  //Schön 
    cout<<*(arr[5]+7)<<'\n';  //Grausam
    cout<<(*(arr+5))[7]<<'\n';  //Auch grausam
    cout<<*(*(arr+5)+7)<<'\n';  //Igitt
    

    Zum Abschluss gibt's noch ein Listing mit dyn. 3D Arrays. Wie bei den Matrizen arbeiten wir uns beim Erstellen von außen nach innen vor, beim Löschen wird umgekehrt vorgegangen:

    #include <iostream>
    
    using namespace std;
    
    template<typename T>  //3D-Array ausgeben
    void print3D(T **mat, size_t x, size_t y, size_t z) {
      for (size_t i=0;i<x;++i)
        for (size_t j=0;j<y;++j)
          for (size_t k=0;k<z;++k)
    	cout<< *(*(*(mat+i)+j)+k) <<'\n';
    };
    
    template <typename T>  //3D-Array erstellen
    T ***create3D(size_t x, size_t y, size_t z) {
      //Im Prinzip haben wir hier nur ein Array(T***) mit T** Elementen wobei 
      //jedes dieser T** ein weiteres Array von T* enthält.
      //Vllt. geht das auch einfacher???
      T ***arr=0;
    
      for (size_t i=0;i<x;++i) 
        arr = new T**[x];
    
      for (size_t i=0;i<y;++i)
        *(arr+i) = new T*[y];
    
      for (size_t i=0;i<y;i++)
        for (size_t j=0;j<z;++j) 
         *(*(arr+i)+j) = new T[z];
    
      return arr;
    };
    
    template <typename T>  //3D-Array wieder löschen
    void kill3D(T ***mat, size_t x, size_t y, size_t z) {
      for (size_t i=0;i<y;i++)
        for (size_t j=0;j<z;++j) 
          delete [] *(*(mat+i)+j);
    
      for (size_t i=0;i<y;++i)
        delete [] *(mat+i);
    
      delete [] mat;
      mat = 0;
    };
    
    int main(int argc, char **argv) {
      size_t x=10,y=10,z=10;
    
      int ***mat = create3D<int>(x,y,z);
    
      for (size_t i=0;i<x;++i)
        for (size_t j=0;j<y;++j)
          for (size_t k=0;k<z;++k)
    	mat[i][j][k] = i+j+k;
    
      print3D(mat,x,y,z);
    
      kill3D(mat,x,y,z);
    
      return 0;
    };
    

    Hoffentlich wurde etwas Klarheit in dieses Sternchen-minenfeld gebracht, wenn nicht, kann man ja nachfragen.



  • Also gut, der Artikel ist fertig. Jetzt wird er mit E gepostet?!? (Oder doch mit X ?? Bin verwirrt...)



  • Das, was ich verschieben werde bekommt ein [E]. (Hast es also richtig gemacht.)
    Dieser hier bekommt ein [X].

    Und du denkst noch an deine Autorenvorstellung? 😉



  • estartu_de schrieb:

    Das, was ich verschieben werde bekommt ein [E]. (Hast es also richtig gemacht.)
    Dieser hier bekommt ein [X].

    Puh, glück gehabt 😃

    Und du denkst noch an deine Autorenvorstellung? 😉

    Ja, mache ich. Auf Bildchen müsst ihr noch ein wenig warten. Sehe Photographie als Versuch der Menschen, die Vergänglichkeit des Moments zu umgehen 😉 😉
    Nee, quatsch, muss nur erst wieder Fotos machen, hat ja noch Zeit.


Anmelden zum Antworten