peace software
  Berndt Wischnewski  Richard-Wagner-Str. 49  10585 Berlin 
  Tel.: 030 - 3429075  FAX : 030 34704037  email: webmaster@peacesoftware.de  Ust-ID: DE135577379 
<= Previous

 

C Kurs - wenn der Pointer in den Wald zeigt

Was sind Pointer? Pointer sind Variablen und sie sind Adressen, es sind Variablen die eine Speicheradresse aufnehmen können, deshalb sind sie in 32 Bit Betriebssystemen auch 4 Bytes groß, entsprechen also unsigned long integer.

Das Verständniß von Pointern ist wirklich wichtig um C richtig zu verstehen und programmieren zu können. Und zwar gleich aus drei Gründen:

  • Pointer ermöglichen es, das Funktionen ihre beim Aufruf übergebenen Variablen verändern können

  • dynamische Verwaltung von Speicherplatz, memory managment, funktioniert immer über Pointer

  • wenn man sein Programm optimieren will, Geschwindigkeit, Speicherbedarf sind Pointer immer sehr beliebt

Das waren die Vorteile, man kann mit Pointern aber auch sehr einfach die größten und undurchschaubarsten Fehler produzieren. Wenn Ihr Programm auf Ihrem Rechner fehlerfrei läuft, aber Ihr Kollege, Kunde, Bekannter behauptet steif und fest, es stürze bei ihm dauernd ab, dann suchen Sie den Fehler zuerst bei Ihren Pointern. Pointer, das Konzept dabei und die Umsetzung davon gehören zu den komplexesten Dingen beim C programmieren, also passen Sie gut auf, jetzt beim Kurs sowieso und natürlich auch später beim programmieren.

 

1. Deklaration und Benutzung von Pointern

Jetzt wissen Sie, dass Pointer einerseits wichtig sind, andererseits aber ein Quell ständiger Freude bei der Fehlersuche darstellen. Nun sehen wir uns doch mal die Syntax zum Deklarieren und Benutzen von Pointern an:

Bei der Deklaration einer Pointer-Typ Variable steht ein '*' vor dem Variablennamen. Das Sternchen '*' zeigt in diesem Fall an, dass es sich um einen Pointer handelt, es hat nichts mit der Multiplikation zu tun. Von allen Datentypen können auch Pointer deklariert werden,
datentyp *pointervar;
hier z.B. deklariert man einen Pointer auf eine integer Ganzzahl:

// Lieber C Compiler, erstelle mir eine Variable, die einen Pointer auf eine Integervariable aufnehmen kann:
int *intptr = NULL;
// die Variable intptr kann eine Adresse eines integer Wertes aufnehmen.
// Am besten man initialisiert die Variable gleich auf NULL.
...

// Compiler, erstelle mir eine Integervariable und weise ihr den Wert 123 zu:
int a = 123;

Pointer ohne Datentyp: will man dem Pointer keinen bestimmten Datentyp zuweisen, deklariert man ihn als void.

void *genericPointer;

Die Größe eines generischen, nicht auf einen bestimmten Datentyp festgelegten Pointers entspricht der Größe einer Speicheradresse des Betriebssystems, 32-Bit -> 4 Bytes; 64-Bit -> 8 Bytes.



Mit dem Kaufmanns-und '&' Operator weist man dem Pointer die Adresse einer Variablen zu,
pointervar = &variable.
In der Pointervariablen steht danach die Speicheradresse der zugewiesenen Variablen, der Pointer referenziert jetzt die Variable, er zeigt auf die Variable. Noch ein Hinweis, der & Operator für die Pointer Referenzierung hat nichts mit dem logischen UND Operator, ebenfalls & zu tun:

intptr = &a; // weise jetzt dem Integerpointer intptr die Adresse der Integervariablen a zu.
printf("%d\n", a); // a wird angezeigt es sollte '123' dort stehen

Der Pointer intptr enthält jetzt die Adresse der Variablen a, intptr referenziert a oder intptr zeigt auf a.

Mit Sternchen '*' Operator erhält man das, was in der Speicheradresse steht, die der Pointer enthält, man dereferenziert den Pointer:
integervar = *pointer_auf_int_var;
man schreibt in eine andere Variable das, was an der Speicheradresse steht, auf die der Pointer zeigt,oder auch umgekehrt
*pointer_auf_int_var = 123;
man schreibt an die Speicheradresse, auf welche der Pointer zeigt, einen anderen Wert. Damit das ganze funktioniert, müssen die Variable und Pointer vom gleichen Datentyp sein, also z.B. integer;

*intptr = 321; // und schreibe jetzt in die Adresse, auf die intptr zeigt, einen neuen Wert, 321
printf("%d\n", a); // a wird noch mal angezeigt, es sollte jetzt '321' dort stehen, obwohl wir a überhaupt nicht angefasst haben

 

Und jetzt das ganze noch einmal als Graphik:

Pointer Graphik

 

Nachtrag: seit kurzem nenne ich einen neuen Mac Pro mit 64 bit Betriebssystem mein eigen, da sind die Adressen 8 Byte groß ;) .

 

2. Pointer Arithmetik

Speicheradressen sind auch nur Zahlen und mit Zahlen kann man rechnen, da jedoch beim Rechnen mit Adressen alles andere als Plus und Minus unsinnig erscheint, sind auch nur diese beiden Operationen erlaubt, warum solte jemand eine Adresse durch etwas teilen oder mit einer anderen Zahl multiplizieren? Alle Rechenoperationen außer Plus und Minus verursachen bei Pointern Fehlermeldungen.

So nun rechnen wir doch mal was mit Pointern:

double *d; // Pointer deklarieren

d = 420000; // dem Pointer irgenteine Adresse zuweisen
printf("1. Pointer: %d\n", d); // jetzt mal den Inhalt der Pointer Variablen ausdrucken
d = d + 3; // 3 dazu adieren
printf("2. Pointer: %d\n", d); // jetzt noch mal ausdrucken und dann erstaunt gucken

Wenn man den kleinen Test laufen läßt, erhält man folgenden Ausdruck auf dem Bildschirm:

1. Pointer: 420000
2. Pointer: 420024

Zu dem ursprünglichen Wert der Pointervariablen wird nicht etwa 3 addiert, wie es eigentlich dasteht, sondern 3 mal dem Speicherbdarf des Pointertyps. Nochmal, erste Zeile, double *d; unser Pointer, *d zeigt auf den Datentyp double, double ist 8 Bytes groß. Wenn ich jetzt zu irgendeiner Adresse, z.B. 420000, die in meiner Pointervariablen d steht, 3 dazu addiere, erhalte ich als Ergebnis 420024, also 420000 plus 3 * 8, Ursprungsadresse plus 3 mal Größe des Datentyps. Das selbe funktioniert auch mit dem ++ oder -- Operator, ebenso += und -= .

Bei Character Pointern, also Pointer die auf ein Zeichen zeigen, ist der Datentyp nur ein Byte groß, hier stimmt die Arithmetik dann sowieso wieder.

 

3. Pointer und Arrays

Es gibt eine sehr enge Beziehung zwischen Pointern und Arrays, sie liegt darin begründet, dass Arrayvariablen eigentlich Pointer sind. Ein Array ist ein Pointer auf das erste Element des Array. Sie können Arrayvariablen direkt an einem Pointer zuweisen oder sie als Pointer an Funktionen übergeben. Hier mal ein kleines Beispielprogramm:

#include <stdio.h>

// 'PrintArray', eine Funktion die als Eingabevariablen einen Pointer auf einen integer Wert,
// 'int *inputArray', sowie eine simple integer Variable verlangt
void PrintArray(int *inputArray, int arraysize)
{
  int i;
  for(i = 0; i < arraysize; i++)
  {
    printf("%d\n", inputArray[i]); // der integer Pointer inputArray wird einfach als Array behandelt
  }
}


int main (int argc, const char * argv[])
{
  int array[5] = {10, 20, 30, 40, 50}; // Deklaration eines Array mit 5 Elementen
  int *arrayStart = NULL;

  arrayStart = array;
  printf("%ul\n", arrayStart); // auf welche Adresse zeigt 'arrayStart'?
  printf("%d\n", *arrayStart); // und was steht an dieser Adresse?
  printf("%ul\n", array);      // was steht eigentlich in 'array'
  printf("%ul\n",&array[0]);   // und welche Adresse hat das erste Element des Arrays?

  PrintArray(array, 5); // die Funktion PrintArray erwartet als ersten Parameter eine Pointer auf integer,
                        // da eine Arrayvariable eigentlich ein Pointer ist,
                        // können Sie diesen hier einfach übergeben.


  return 0;
}

 

4. Verändern von an die Funktion übergebenen Variablen, Call bei Reference

Bis jetzt hatten wir immer nur einfache Variablen an Funktionen übergeben, z.B so:

void MeineLieblingsfunktion(int inputvar)
{
  inputvar = inputvar * 10;
  printf("inputvar: %d\n", inputvar);
}


int main(void)
{
  int x = 42;
  MeineLieblingsfunktion(x);
  printf("x: %d\n", x);
}

Man folgenden Ausdruck auf dem Bildschirm:

inputvar: 420
x: 42

Innerhalb von ' MeineLieblingsfunktion' wird die Eingangsvariable ' inputvar' verändert, aber nur lokal in der Funktion. Die Variable ' x' in der übergeordneten, aufrufenden Funktion hat anschließend immer noch den selben Wert wie vor dem Funktionsaufruf. Wie funktioniert das? Nun der C Compiler erzeugt eine Kopie der Eingangsvariablen. Die Funktion arbeitet nun mit dieser Kopie und kann damit machen wass sie will, die Variable in der aufrufenden Funktion bleibt unberührt. Diese Vorgehensweise nennt man auf gut deutsch Call By Value, der Wert der Variablen wird an eine Funktion übergeben, nicht die Variable selber.

Meist ist dies auch das gewünschte Verhalten, trotzdem will oder muss man auch die an die Funktion übergeben Variable verändern können, nur wie? Nun, Sie übergeben einfach einen Pointer an die Funktion, dieser zeigt auf die echte Speicheradresse Ihrer Variablen und dann können Sie diese verändern:

void FunktionMitPointer(int *inputvar)
{
  *inputvar = *inputvar * 10; // der Speicherbereich, auf den der Pointer zeigt, wird verändert
  printf("*inputvar: %d\n", *inputvar);
}


int main(void)
{
  int x = 42;
  FunktionMitPointer(&x); // die Adresse von x wird an die 'FunktionMitPointer' übergeben
  printf("x: %d\n", x);
}

Man folgenden Ausdruck auf dem Bildschirm:

*inputvar: 420
x: 420

Wie man sieht, wurde die Variable ' x' in der aurufenden Funktion verändert, die Adresse der Variablen x wird mit dem Kaufmannns-und in die Funktion
FunktionMitPointer(&x); hineingegeben und dort mit dem * Operator dereferenziert und verändert. Weil man hier nicht die Variable sondern die Referenz, die Adresse der Variablen, übergibt nennt sich das ganze Call By Reference.


5. Pointer auf Pointer

Um die Sache zu verkomplizieren, kann man sich auch Pointer deklarieren, welche auf einen anderen Pointer zeigen, wird z.B. häufig angewendet, wenn man eine Anwendung mit eigenem memory managment schreibt. Kurz nebenbei erklärt, man benutzt einen Pointer, der auf die Daten zeigt, werden die Daten im Speicher verschoben, wird dieser Pointer aktualisiert. Dann deklariert man sich einen Masterpointer, welcher auf diesen Datenpointer zeigt. Der Masterpointer verändert sich nicht, er zeigt immer nur auf den anderen Pointer, man kann ihn zu jeder Zeit an alle Funktionen übergeben. Um an die Daten zu kommen, muß man ihn nur zweimal dereferenzieren.

Man deklariert einen Pointer auf einen Pointer mit zwei Sternchen '**' , hier mal ein Beispiel:

int x = 42;
int *myptr, **masterptr;

myptr = &x; // myptr zeigt auf x
masterptr = &myptr; // masterptr zeigt auf myptr
printf("**masterptr: %d\n", **masterptr); // masterptr wird zweimal dereferenziert

Man kann das natürlich noch weitertreiben und Pointer auf Pointer zeigen lassen, die wiederum auf Pointer auf Pointer zeigen:

int x = 42, *ptr1, **ptr2, ***ptr3, ****ptr4; // usw.

ptr1 = &x;
ptr2 = &ptr1;
ptr3 = &ptr2;
ptr4 = &ptr3;

printf("****ptr4: %d\n", ****ptr4); // ergibt: ****ptr4: 42

ist zwar korrekter C Kode, bis jetzt hatte ich aber noch keine sinnvolle Anwendung für sowas gehabt.

 

 

6. Wenn der Pointer in den Wald zeigt

Wie schon am Anfang erwähnt, kann man mit Pointern die undurchschaubarsten Fehler produzieren. ich zeige Ihnen jetzt wie das geht:

int *myptr;

*myptr = 1234;

OK, man deklariert myptr , in diesem Moment steht entweder eine zufällige beliebige Adresse in myptr, der Pointer zeigt in den Wald, oder der Compiler initialisiert ihn mit NULL, in dem Fall zeigt er auf den Startbereich des Speichers, dorthin wo das Betriebsystem Ihres Computers anfängt. In der nächsten Zeile schreibt man an diese Speicheradresse einen Wert, '1234'.

Wenn der Pointer wie im zweiten Fall auf NULL zeigt, bekommt man sofort eine 'NULL-POINTER-EXCEPTION' Fehlermeldung, d.h. das Betriebssystem beschwert sich, das ist noch einfach. Im ersten Fall, der Pointer zeigt einfach irgentwohin, können drei Sachen passieren:

  • der Pointer zeigt auf eine Adresse außerhalb des Adressbereiches des Programmes, eventuell sogar auf eine Adresse die physikalisch gar nicht im Speicher vorhanden ist, nicht jeder hat 4 Gigabyte Memory in seinem Rechner stecken. Man erhält eine 'SEGMENT-FAULT' Fehlermeldung, das System meldet das Sie unerlaubter weise versuchen auf eine Speicheradresse außerhalb des Ihrem Programmes zugewiesenen Speicherbereiches zuzugreifen.

  • der Speicherplatz, auf den der Pointer zeigt, ist einfach frei, man merkt erst mal gar nichts.

  • der Speicherplatz, auf den der Pointer zeigt, ist von einer Variablen, Array oder sonstwas belegt, hier sollte man jetzt ein Fehlverhalten des Programmes bemerken.

Hier lässt sich jetzt auch dieses machmal gehts - manchmal gehts nicht Verhalten eines Programmes erklären. Die Adressen werden zur Laufzeit des Programmes zugewiesen, wenn der Rechner viel Speicherchips zur Verfügung hat oder der Speicher nur wenig ausgenutzt wird, sprich nur wenige Programme laufen, wird die Chance größer, das der Pointer auf freien Speicherplatz zeigt und das Programm erstmal problemlos läuft. Wird es dann von jemand andrem auf einem Rechner mit wenig freien Speicher benutzt, kann es sein das der nicht initialisierte Pointer dann auf etwas wichtiges zeigt und sich das Programm unsinnig verhält oder abstürzt.

Besser ist jetzt folgende Konstruktion:

int *myptr = NULL; // die Pointervariable erstmal auf NULL setzen
int x = 42;

myptr = &x; // dann den Pointer initialisieren

...

if(myptr) // vorher prüfen ob der Pointer zugewiesen ist
  *myptr = 1234;

Die meisten modernen C Compiler setzen Pointervariablen erstmal auf NULL, schauen Sie sich jetzt aber mal folgendes Codefragment an:

int *myptr = NULL; // die Pointervariable erstmal auf NULL setzen
int x[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // einen Array von 10 Integern anlegen

myptr = &x[5]; // dann den Pointer auf das 6. Element des Array zeigen lassen

...

myptr += 100; // den Pointer auf 100*4 Speicherplätze weiter oben zeigen lassen

Sie haben es bestimmt verstanden. Der Integerarray hat nur 10 Element und der Pointer wird, ausgehend von der Adresse des 5. Elementes des Array, auf eine Adresse die 400 Bytes größer ist, gesetzt. 400 Bytes, weil integer 4 Byte groß ist, mal 100. Der Pointer zeigt wieder mal in den Wald, diesmal ist die Chance das er noch ins gleiche Segment zeigt, sogar recht groß. Wahrscheinlich gibt es keinen SEGMENT-FAULT, was an der Speicherstelle, auf die der Pointer jetzt zeigt, steht, ist wie vorher dem Zufall überlassen.

 

Ein weiterer netter Fehler ist es den Pointer auf freigegebenen Speicherplatz zeigen zu lassen:

void main(void)
{
  int x;
  int *ptr = NULL;

  SomeFunction(ptr);
  
  ...

  x = *ptr;
  *ptr += 100;
}


void SomeFunction(int *inputPtr)
{
  int localx = 42;

  inputPtr = &localx;
}

Hier passiert folgendes, man legt sich einen Pointer an: int *ptr = NULL;
übergibt diesen an eine Funktion: SomeFunction(ptr);
In dieser Funktion wird der Pointer auf die Adresse einer lokalen Variable dieser Funktion gesetzt: inputPtr = &localx;
und nach dem Verlassen der Funktion wird der Pointer weiter benutzt: x = *ptr;

Das Problem ist bloß, dass die lokale Variable localx nach dem Verlassen der Funktion SomeFunction gar nicht mehr existiert, der Speicherplatz der lokalen Variablen der Funktion wird nach Beenden der Funktion wieder freigegeben. Diesen Fehler nennt man einen 'dangling pointer' oder 'hängenden Pointer' . Ob der Speicherplatz schon wieder benutzt wurde oder nicht ist jetzt wieder dem Zufall überlassen, d.h. das Programm hat einen Fehler, kann aber machmal doch funktionieren.

Also immer daran denken "double check your damn pointers".

 

<= Previous











Alles über Pointer: