Zeiger - Ein unersetzliches Hilfsmittel

Einleitung

Jetzt ist es soweit! Das Thema, wovor sich alle angehenden Programmierer fürchten. Das Thema, wo so viele sagen, dass es dermaßen unverständlich und unlogisch ist, dass niemand es verstehen kann. Doch eigentlich ist ein Zeiger nicht mehr als eine normale Variable. Der einzige Unterschied ist, dass man in diese Variable die Speicheradresse einer anderen Variable hineinschreibt. Jetzt werden sich einige vielleicht fragen wozu das Ganze, doch nach ein paar Beispielen dürften jedem die Vorteile verständlich sein.

Wer bereits mit Funktionen programmiert hat, ist sicherlich schon einmal darauf gestoßen, dass es doch ganz praktisch wäre, wenn man mehr als einen Wert zurückgeben könnte. Mit Zeigern kann man das ganz einfach realisieren. Man muss nur der Funktion die Adresse(= Zeiger) auf einen Speicherplatz übergeben. Über diese Adresse kann man nun in der Funktion einen Wert auf diese Speicherstelle schreiben und kann diese Variable dann auch außerhalb der Funktion weiterverwenden.

Ein weiterer Vorteil ist die Ersparnis von unnötig großen Kopiervorgängen. Wenn man einer Funktion zum Beispiel zehn Integer übergeben will, dann kann man entweder alle diese Werte übergeben, oder man verwendet Zeiger und übergibt nur einen Zeiger auf den Beginn eines Arrays mit zehn Integern. Bei einem 32-Bit Computer muss man somit nur mehr 4 Byte für den Zeiger statt normalerweise 10*4 Bit für die einzelnen Integer übergeben. Das sieht zwar vielleicht nicht viel aus, doch bei einer großen Anzahl kann man dadurch schon einiges an Zeit einsparen.

Wie definiert man einen Zeiger?

Einen Zeiger anzulegen funktioniert eigentlich genauso wie das Anlegen einer Variablen. Der einzige Unterschied ist, dass man vor den Zeigernamen noch ein * hinschreibt.

int *int_zeiger; // Ein Zeiger auf einen Integer
char *char_zeiger, *noch_ein_char_zeiger; // Zwei char-Zeiger
// ACHTUNG:
char *char_zeiger, char_kein_zeiger; // Ein char-Zeiger und eine normal char-Variable

Wie man sieht ist es ganz einfach einen Zeiger anzulegen. Doch man sollte aufpassen, dass man, wenn man mehrere Zeiger deklariert, auch vor jeden ein * schreibt. Ansonsten würde man, wie im zweiten char-Beispiel, nur einen Zeiger deklarieren. char_kein_zeiger wäre einfach nur eine normale Variable vom Typ char.

Und worauf zeigt er jetzt?

Jetzt hat man zwar ein paar Zeiger deklariert, aber verwenden kann man sie noch nicht, da sie noch auf irgendeine undefinierte Speicherstelle zeigen. Wenn man jetzt versuchen würde auf den Speicher zuzugreifen, auf den der Zeiger zeigt, dann ist die Wahrscheinlichkeit sehr groß, dass man einen berüchtigten Segmentation Fault bekommt. Um das zu verhindern, sollte man entweder gleich eine Adresse angeben, auf die der Zeiger zeigen soll, oder man weist ihm NULL (oder auch nur 0) zu, und bekommt dadurch einen sogenannten Nullzeiger. Der Vorteil von einem Nullzeiger gegenüber einem undefinierten Zeiger ist, dass wenn man ihn versehentlich verwendet sicher einen Segmentation Fault bzw. eine Nullpointer Exception bekommt, und nicht zufällig eine gültige Speicheradresse erwischen kann und dadurch sehr schwer zu findende Fehler erzeugt.

int *null_zeiger = NULL, *noch_einer = 0; // Zwei integer-Nullzeiger

Um den Zeiger jetzt auf eine definierte Speicherstelle zeigen zu lassen muss man sich die Adresse einer Variablen holen und in dem Zeiger speichern. Dazu gibt es den Adressoperator &. Mit ihm erhält man die Adresse einer Variablen.

int normaler_integer = 5; // Eine normale Integerzahl
int *int_zeiger = &normaler_integer; // Der Zeiger zeigt auf den Integer

Durch den Adressoperator und die Zuweisung zeigt der Zeiger jetzt auf den Speicherplatz an dem der Integer gespeichert ist.

Und wie kann man auf das Ziel des Zeigers zugreifen?

Bis jetzt kann man mit dem Zeiger noch nicht viel machen, da man noch nicht auf die Speicherstelle zugreifen kann, auf die der Zeiger zeigt. Doch genau dafür gibt es den Dereferenzierungsoperator *. Mit ihm kann man auf den Wert zugreifen, der an der Stelle im Speicher steht, dessen Adresse im Zeiger gespeichert ist.

int zahl = 5;
int *zeiger = &zahl; // Dem Zeiger die Adresse von zahl zuweisen
int zweite_zahl = *zeiger; // zweite_zahl bekommt den Wert 5 da durch * auf den Wert am Zeigerziel zugegriffen wird.

Beispiele zu Zeigern

Hier ein paar (mehr oder weniger) knifflige Beispiele zu Zeigern. Am Ende des Beispiels finden sie Ergänzende Angaben zu den Adressen. In der Tabelle fehlen jedoch einige Werte, die man aus dem Programm lesen können sollte. Lösungen und Erklärungen befinden sich weiter unten im Text.

Beispiel 1

int a, *p;
 
a=5;
p=&a;
Name Wert Adresse
a ? 4400
p ? 4404

Beispiel 2

int x, y, *z;
 
x=5;
z=&x;
y=*z;
Name Wert Adresse
x ? 4400
y ? 4404
z ? 4408

Beispiel 3

float a, b, *pa;
 
pa=&a;
*pa=12;
*pa+=5.5;
b=*pa+5;
Name Wert Adresse
a ? 4400
b ? 4408
pa ? 4416

Beispiel 4

void fswap (float *, float *);
 
int main ()
{
 
  float x=5.5, y=7.5;
 
  …
 
  fswap (&x, &y);
 
  …
 
  return 0;
 
}
 
void fswap (float *p1, float *p2)
{
 
  float temp;
 
  temp=*p1;
  *p1=*p2;
  *p2=temp;
 
}
Name Wert Adresse
x ? 4400
y ? 4408
Name Wert Adresse
p1 ? 4416
p2 ? 4424
temp ? 4432

Beispiel 5.1

int a []={10, 20, 30, 40};
int *pa;
 
pa=a;
Name Wert Adresse
a[0] ? 3350
a[1] ? 3354
a[2] ? 3358
a[3] ? 3362
*pa ? 3366
a ? 3370

Beispiel 5.2

int i=2, a[]={10, 20, 30, 40}, *pa;
 
pa=a;
 
a[i]=5;		
*(pa+i)=5;
Name Wert Adresse
a[0] ? 3350
a[1] ? 3354
a[2] ? 3358
a[3] ? 3362
*pa ? 3366
a ? 3370

Lösungen und Erklärungen zu den Beispielen

Lösung Beispiel 1

Name Wert Adresse
a 5 4400
p 4400 4404
a=5;
p=&a;

a bekommt den Wert 5 und p die Adresse (Adressoperator &) von a zugewiesen.

Lösung Beispiel 2

Name Wert Adresse
x 5 4400
y 5 4404
z 4400 4408
x=5;

x bekommt den Wert 5.

z=&x;

z zeigt auf x.

y=*z;

y bekommt den Wert auf den z zeigt.

Kurz gesagt: y=x;

Lösung Beispiel 3

Name Wert Adresse
a 17.5 4400
b 22.5 4408
pa 4400 4416
pa=&a;

Der Zeiger pa zeigt nun auf a.

*pa=12;

*pa (=Wert auf den pa Zeigt) wird 12. Also bekommt a den Wert 12.

*pa+=5.5;

*pa wird um 5.5 erhöht, 12+5.5=17.5

b=*pa+5;

b bekommt den Wert von *pa+5 zugewiesen, b ist also 22.5.

Lösung Beispiel 4

Name Wert Adresse
x 5.5 –> 7.5 4400
y 7.5 –> 5.5 4408
Name Wert Adresse
p1 4400 4416
p2 4408 4424
temp 5.5 4432
float x=5.5, y=7.5;

x und y werden zu Beginn mit 5.5 und 7.5 initialisiert.

fswap (&x, &y);

Die Adressen der beiden Variablen werden an die Funktion fswap übergeben. Das heißt, man erhält in der Funktion zwei Pointer auf diese Variablen.

temp=*p1;

temp bekommt den Wert auf den *p1 zeigt. p1 ist ein Pointer auf x und deshalb bekommt temp den Wert von x, also 5.5.

*p1=*p2;

Der Wert auf den *p1 zeigt, wird mit dem von *p2 überschrieben. x bekommt also den Wert von y.

*p2=temp;

*p2 bekommt den Wert von temp. Das heißt, y bekommt den ursprünglichen Wert von x.

Die Variablen wurden also vertauscht.

Lösung Beispiel 5.1

Name Wert Adresse
a[0] 10 3350
a[1] 20 3354
a[2] 30 3358
a[3] 40 3362
*pa 10 3366
a 3350 3370
int a [4]={10, 20, 30, 40};

Die 4 Elemente von a werden mit 10, 20, 30 und 40 belegt.

pa=a;

pa zeigt nun auf a. Da jedoch der Feldname ohne Indexklammern (=[]) die Adresse des 1. Feldelementes ist, zeigt pa auf a[0]. a ist ein Konstanter Zeiger und kann nicht verändert werden!

Lösung Beispiel 5.2

Name Wert Adresse
a[0] 10 3350
a[1] 20 3354
a[2] 30 –> 5 –> 5 3358
a[3] 40 3362
*pa 10 3366
a 3350 3370
a [i]=5;

a[2] bekommt den Wert 5.

*(pa+i)=5;

pa zeigt auf das 1 Element a[0]. Wird der Wert von pa erhöht, zeigt es auf das nächste Element. In diesem Fall also a[2]. a[2] wird dadurch abermals der Wert 5 zugewiesen.