[X] Pointer in C(++)



  • 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 einen ungültigen Speicherzugriff ("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 (Bjarne Stroustrup widmet dieser Entscheidung auch einen FAQ-Eintrag). 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 (zumindest oberflächlich nicht, intern werden Referenzen auch über Pointer realisiert). Der wichtigste Unterschied ist wohl, dass Referenzen immer nur einen anderen Namen für das referenzierte Objekt darstellen und sich so verhalten, als belegten sie keinen eigenen Speicher (der Adressoperator auf eine Referenz angewandt liefert die Adresse des referenzierten Objekts). 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 neu zuzuweisen. Im Vergleich zu Zeigern können Referenzen nicht 0 sein, sie müssen aber auch nichts referenzieren, es wird nur von Ihnen erwartet.

    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 wie ein konstanter Pointer auf das erste Element im Array behandelt werden kann. 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 referenzierte Objekt 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. Ein modernes 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 kann 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.



  • Da der originale Artikel auf mysteriöse Weise im Nirvana verschwunden ist, wird er re-released. Bitte nochmal auf Fehler überprüfen.



  • 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 einen ungültigen Speicherzugriff ("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 (Bjarne Stroustrup widmet dieser Entscheidung auch einen FAQ-Eintrag). 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 Übergabe by Reference möchte ich kurz Referenzen (nur C++) ansprechen, die den Zeigern zwar ähneln, aber keine sind (zumindest oberflächlich nicht, intern werden Referenzen auch über Pointer realisiert). Der wichtigste Unterschied ist wohl, dass Referenzen immer nur einen anderen Namen für das referenzierte Objekt darstellen und sich so verhalten, als belegten sie keinen eigenen Speicher (der Adressoperator auf eine Referenz angewandt liefert die Adresse des referenzierten Objekts). 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 neu zuzuweisen. Im Vergleich zu Zeigern können Referenzen nicht 0 sein, sie müssen aber auch nichts referenzieren, es wird nur von Ihnen erwartet.

    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 wie ein konstanter Pointer auf das erste Element im Array behandelt werden kann. 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 adressieren, 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 referenzierte Objekt 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 bekommen (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. Ein modernes 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 freigeben!
    Nach dem Freigeben 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'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. Dass 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 kann 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 ist
    
    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.



  • Dieser Thread wurde von Moderator/in GPC aus dem Forum Die Redaktion in das Forum Archiv verschoben.

    Im Zweifelsfall bitte auch folgende Hinweise beachten:
    C/C++ Forum :: FAQ - Sonstiges :: Wohin mit meiner Frage?

    Dieses Posting wurde automatisch erzeugt.


Anmelden zum Antworten