communityWir suchen ständig neue Tutorials und Artikel! Habt ihr selbst schonmal einen Artikel verfasst und seid bereit dieses Wissen mit der Community zu teilen? Oder würdet ihr gerne einmal über ein Thema schreiben das euch besonders auf dem Herzen liegt? Dann habt ihr nun die Gelegenheit eure Arbeit zu veröffentlichen und den Ruhm dafür zu ernten. Schreibt uns einfach eine Nachricht mit dem Betreff „Community Articles“ und helft mit das Angebot an guten Artikeln zu vergrößern. Als Autor werdet ihr für den internen Bereich freigeschaltet und könnt dort eurer literarischen Ader freien Lauf lassen.

Pointer in C(++) PDF Drucken E-Mail
Benutzerbewertung: / 18
SchwachPerfekt 
Sonntag, den 18. Dezember 2005 um 18:02 Uhr

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 mit Pointer
  3. Übergabe/Rückgabe von Pointer
  4. Referenzen
  5. Zeigerarithmetik
  6. const bei Pointer
  7. Speicher allokieren
  8. 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 Dereferenzieren und geschieht mit dem *-Operator (dies ist in diesem Fall der Indirektions-/Dereferenzierungsoperator, der nicht mit dem Multiplikationsoperator verwechselt werden sollte):

#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;
}

4 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 8.

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.

5 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);
}

6 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.

7 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 (ein Aufruf von ptr=(int*)realloc(ptr,0); hat denselben Effekt).

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 ein Beispiel in C:

//Einzelnes int dynamisch allokieren:
int *ptr = 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;
scanf("%d", &size);
 
//Speicher für size ints anfordern
int *array = malloc(sizeof(int)*size); 
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.

8 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 = 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) = 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;
 
    arr = new T**[x];
 
    for (size_t i=0;i<x;++i)
        *(arr+i) = new T*[y];
 
    for (size_t i=0;i<x;i++)
        for (size_t j=0;j<y;++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.

Zuletzt aktualisiert am Mittwoch, den 10. Oktober 2007 um 21:03 Uhr
 
AUSWAHLMENÜ