Eine kleine Einführung in den Umgang mit Pointern.
Normale Variablen
Wir definieren zwei normale Variablen vom Typ int. a weisen wir direkt einen Wert zu, b nicht.
int a = 7;
int b;
Mit den folgenden beiden Zeilen können wir uns die Werte von a und b ausgeben lassen. Wie sie sehen enthält die nicht explizit zugewiesene Variable b bei jedem Start der Anwendung einen anderen Wert, manchmal auch 0. Das bedeutet die Variable wird im Gegensatz zu anderen Sprachen nicht automatisch auf 0 gesetzt.
printf("a = %i", a); //Ausgabe: a = 7
printf("b = %i", b); //Ausgabe: b = 37575840
Um noch etwas mehr über unsere Variablen zu erfahren, können wir mit der Anweisung &a die Speicheradresse der Variable a ausgeben. Hier eine Ausgabe von Wert und Adresse.
printf("a = %i; &a = %i", a, &a);
//Ausgabe: a = 7; &a = 1800745048
Die Form der Speicheradresse variiert je nach Plattform bzw. Betriebssystem. Auf einem 64-bit Windows ist die Speicheradresse eine Zahl mit einer Länge von 64 Bit bzw. 8 Byte.
Wird eine normale Variable als Parameter an eine Funktion übergeben, dann wird eine Kopie dieser Variable erzeugt. Man kann also innerhalb der Funktion nicht die originale Variable verändern sondern nur die Kopie.
void passByValue(int value)
{
printf("value = %i; &value = %i", value, &value);
value = 42;
printf("value = %i; &value = %i", value, &value);
}
int a = 7;
printf("a = %i; &a = %i", a, &a);
//Ausgabe: a = 7; &a = 1800000048
passByValue(a);
//Ausgabe: value = 7; &value = 1800333300
//Ausgabe: value = 42; &value = 1800333300
printf("a = %i; &a = %i", a, &a);
//Ausgabe: a = 7; &a = 1800000048
Die Konsequenz daraus ist, dass beim Aufruf von Funktionen zusätzlicher Speicherplatz belegt wird. Gerade bei sehr großen Variablen (z.B. die Vertices von 3D Modellen) kann dies unter Umständen zu Problemen führen. Hier kommen Pointer ins Spiel.
Pointer
Ein Pointer ist ein spezieller Datentyp indem eine Speicheradresse abgelegt wird. Ein Pointer auf eine Variable vom Typ int, definiert man mit dem Typenbezeichner int*
Wir definieren hier eine normale Variable a sowie einen Pointer b und vergleichen die Ausgabe.
int a;
int* b;
printf("a = %i; &a = %i", a, &a);
//Ausgabe: a = 31209329; &a = 1800000048
printf("b = %i; &a = %i", b, &b);
//Ausgabe: b = 13045698; &a = 1800000036
Wie sie sehen enthalten beide Variablen einen scheinbar zufälligen Wert. Tatsächlich ist der Wert von a und b aber nicht zufällig sondern es handelt sich dabei um die Bytes die sich an der Stelle im Speicher befanden wo die Variablen erzeugt wurden. Bei jedem Start Ihrer Software wird dort ein anderer Wert erscheinen.
Wenn Sie die Funktion sizeof() aufrufen können Sie außerdem sehen wieviel Speicherplatz die jeweilige Variable benötigt.
int a;
int* b;
// sizeof(a) = 4;
// sizeof(b) = 8;
Wie sie sehen ist ein Integer 4 Bytes groß und der Pointer ist 8 Bytes groß da er eine 64-bit Speicheradresse aufnehmen kann.
Wenn wir versuchen auf b zuzugreifen werden wir in den meisten Fällen einen SEGMENTATION FAULT erhalten. Das bedeutet unsere Anwendung hat versucht auf Speicher zuzugreifen der ihr nicht gehört.
Um sinnvoll mit Pointern zu arbeiten, müssen wir zusätzlich Speicher reservieren auf den unser Pointer zeigen soll. Das tun wir mit der Funktion malloc()
int* b = malloc(4);
Hier bitten wir das Betriebssystem uns 4 byte Speicher zu reservieren. Der Rückgabewert ist die Adresse dieses reservierten Speichers. Unser Pointer enthält nun also die Adresse zu einem Bereich im Speicher in den wir 4 Byte Daten ablegen können.
Die beiden schon bekannten Ausgaben zeigen uns nun die Adresse die im Pointer gespeichert ist und die Andresse wo der Pointer selbst gespeichert ist:
// b = 1491101968 &b = 1839427744
Um auf den reservierten Speicher zuzugreifen, also die Stelle wo wir eigentlich den Integer ablegen können benötigen wir den Dereferenzierungsoperator. Wir schreiben *b
Das folgende Beispiel zeigt wie wir unsere Variable b korrekt mit dem Wert 42 befüllen.
int* b = malloc(4);
*b = 42;
// Ausgabe:
// b = 1401101968
// &b = 1800000036
// *b = 42
Wir können alternativ auch die Adresse einer bereits initialisierten Variable in den Pointer speichern. Das sieht so aus:
int a = 7;
int* b = &a;
// Ausgabe:
// a = 7
// &a = 1800000048
// b = 1800000048
// &b = 1800000036
// *b = 7
Besonders praktisch werden Pointer wenn man Variablen an eine Funktion übergeben möchte und vermeiden möchte, dass dabei eine Kopie der Daten erzeugt wird. Nehmen wir an, eine Funktion erwartet als Parameter einen Pointer auf einen Integer.
void passByPointer(int* value)
{
printf("value = %i; &value = %i; *value = %i", value, &value, *value);
*value = 42;
printf("value = %i; &value = %i; *value = %i", value, &value, *value);
}
Dann kann diese Funktion nun auf value zugreifen und den Wert verändern, so dass dieser auch außerhalb der Funktion geändert ist. Es gibt zwei Möglichkeiten diese Funktion aufzurufen:
int a = 7;
int* b = malloc(4);
*b = 5;
// a = 7 &a = 1800000048
// b = 1401101968 &b = 1800000036 *b = 5
passByPointer(&a); // Entweder Übergabe der Adresse einer Variablen
// value = 1800000048 &value = 1800000077 *value = 7
// value = 1800000048 &value = 1800000077 *value = 42
passbyPointer(b); // Oder Übergabe eines Pointers
// value = 1401101968 &value = 1800000066 *value = 5
// value = 1401101968 &value = 1800000066 *value = 42
// a = 42 &a = 1800000048
// b = 1401101968 &b = 1800000036 *b = 42
Dabei ist zu beachten, dass der eigentliche Pointer genau wie jeder andere übergebene Parameter kopiert wird. Sie sehen dies an der Adresse des Pointers innerhalb des Funktionsaufrufs. In diesem speziellen Fall, indem ein einzelner int* übergeben wird, entsteht also mehr Overhead als beim direkten Übergeben des Wertes. Der Vorteil kommt also erst zum Tragen wenn größere Daten übergeben werden.
Arrays
Arrays stellen eine Spezialform des Pointers dar. Wir initialisieren ein Array mit drei Elementen wie folgt:
Entweder:
int c[3] = {1,2,3};
// oder
int c[3];
c[0] = 1;
c[1] = 2;
c[2] = 3;
Wir könnten aber genau das gleiche erreichen indem wir folgendes schreiben. Tatsächlich ist der folgende Code der, den Ihr Compiler intern produziert.
int* c = malloc(12);
*c = 1;
*(c+1) = 2;
*(c+2) = 3;
Wenn wir dieses Array (egal mit welcher Methode es erzeugt wurde) allerdings an eine Funktion übergeben, dann verhält es sich innerhalb der Funktion immer wie ein Pointer vom Typ int*
Das bedeutet innerhalb der Funktion können wir nicht unterscheiden ob wir einen Pointer auf einen einzelnen Integer erhalten haben oder einen Pointer auf ein Array von Integern.
Deshalb ist das folgende gültiger Code, kann aber zu einer Fehlermeldung führen wenn sie versehentlich über die Grenzen des Arrays hinaus auf Elemente zugreifen wollen.
function callWithArray(int* array)
{
array[0] = 41;
array[1] = 42;
array[2] = 43;
}
int c[3] = {1,2,3};
int d[2] = {1,2};
int e = 1;
callWithArray(c);
// Alles okay
callWithArray(d);
// Für den Compiler okay. Zur Laufzeit ein möglicher Fehler.
callWithArray(&e);
// Für den Compiler okay. Zur Laufzeit ein möglicher Fehler.
Und damit sind wir auch bei dem größten Problem von Pointern. Auf Grund der Leichtigkeit des Zugriffs auf Speicherbereiche die nicht zu der eigentlichen Variablen gehören, können wir andere Teile des Speichers unseres Programms verändern. Solange diese gerade nichts wichtiges enthalten ist das kein Problem aber es kann zu schwer auffindbaren Bugs führen.