27. Pointer I

Pointer sind die effektivste Art, sich in C in den Fuß zu schießen. Ein Pointer ist eine Variable, die auf einen Speicherplatz zeigt und weiß, was für eine Sorte Daten drinsteht.

Leider lassen sich ohne Pointer kaum nichttriviale Programme in C schreiben, weshalb man gut daran tut, sich mit ihnen anzufreunden.

Man sollte über Pointer ähnlich nachdenken wie über Referenzen in Python: In erster Linie verweist ein Pointer auf etwas. Um an das zu kommen, auf was er verweist, muss man in C eben einen Stern vor den Namen des Pointers malen (in Python kommt man gar nicht an nicht-dereferenzierte Referenzen ran, in C ist das einfach die Adresse, an der der quasi “eigentliche Wert” steht). Wenn man sich damit vorstellen kann, dass der eigentliche Wert wiederum eine Adresse sein kann, mithin also ein weiterer Pointer und eine weitere Referenz, hat man die Pointer schon fast in der Tasche.

Definition und Verwendung

Ein Pointer wird definiert, indem man einen Stern vor den Variablennamen setzt:

char *cp;
int *ptrToInt;
int *(*ptrToIntPtr);

Im letzten Beispiel haben wir gleich einen Pointer auf einen Pointer definiert.

Man sieht übrigens häufig Schreibweisen wie int* a;, wodurch der Autor wohl sagen will “Es gibt einen Typ int*, und a ist von diesem Typ”. Diese Idee ist nicht schlecht, verwandelt sich aber in eine böse Falle, wenn man int* a, b definiert – C parst das weiterhin als int *a, b, also ist b vom Typ int. Wir werden später typedef kennen lernen – damit geht so etwas richtig.

Der Wert eines Pointers ist die Adresse der Speicherzelle, auf die er zeigt. Den Wert der Speicherzelle bezüglich des Typs des Pointers erhält man durch Dereferenzieren, wozu der *-Operator dient:

printf("%c %d\n", *cp, **ptrToIntPtr);
*cp = 'a';

Adressoperator

Nach der Definition zeigt der Pointer irgendwohin, und ein Dereferenzieren führt fast sicher zum Absturz. Pointer müssen also auf einen les- und schreibbaren Speicher gerichtet werden. Dazu kann der Adressoperator & dienen, der einen Zeiger auf die dahinterstehende Variable erzeugt:

int a, *ip;
ip = &a;

Häufiger ist die Verwendung mit Arrays. Ein Array ist in etwa ein konstanter Zeiger auf einen Speicherbereich:

int a[20] = {3,2,1}, *ip;
ip = a;

Der Name eines Arrays evaluiert zur Adresse seines ersten Elements. Das wird in der zweiten Zeile des Beispiels benützt. Allgemein gilt für ein beliebiges Array: a = &(a[0]).

Häufig gibt man Pointer aus Funktionen zurück. Da legale Pointer garantiert verschieden vom Nullpointer NULL sind, ist die Konvention hier, dass bei Fehlern in der Funktion NULL zurückgegeben wird, ansonsten der Pointer. Nützlicher Nebeneffekt der Konvention ist, dass Konstrukte wie

if (!(ptr = getAPointer(bla))) {
  /* Fehlerbehandlung */
}

eine relativ kompakte Fehlerbehandlung auch ohne Exceptions ermöglichen.

Funktionspointer

Nicht immer ist es ganz einfach, Pointer zu deklarieren. Funktionspointer etwa gehen eigentlich wie in Python (ein Funktionsname ohne Klammern ist ein Pointer auf die Funktion, der Ampersand ist also unnötig), ihr Prototyp ist aber wegen der Operatorpräzedenz etwas komisch, denn

char *fun(int);

deklariert eine Funktion (die Klammern binden stärker als der Stern), die einen Pointer auf einen char zurückgibt und einen int nimmt. Wenn wir einen Pointer auf eine Funktion, die einen char zurückgibt definieren wollen, müssen wir den Stern enger an den Namen binden, die Deklaration heißt also

char (*fun)(int);

Neben der aus unseren Python-Erfahrungen zu erwartenden Verwendung als Callbacks kann man mit Funktionspointern auch das Verhalten von Funktionen parametrisieren. Denkbar wäre z.B. eine Funktion, die nur Zeichen eines bestimmten Typs (Zahl, Buchstabe) ausgibt:

void printCharsOfType(char *str, int (*isOfType)(int))
{
  while (*str) {
    if (isOfType(*str)) {
      fputc(*str, stdout);
    }
    str++;
  }
  fputc('\n', stdout);
}

Das könnte zusammen mit den Funktionen aus ctype.h verwendet werden:

int main(void)
{
  printCharsOfType("H4ll0, W31t", isdigit);
  printCharsOfType("Hallo, \n\tWelt", isalnum);
  printCharsOfType("Der, welcher . macht, ist %", ispunct);
  return 0;
}

Ausgabe:

4031
HalloWelt
,.,%

Der Umstand, dass ein Funktionsname ohne Klammern ein legaler Ausdruck und damit ein legales Statement ist, ist eine recht fiese Falle. Will man eine Funktion aufrufen und vergisst die Klammern, dann passiert einfach nichts – und der Fehler ist durchaus nicht immer sofort zu sehen. Compiliert man mit -Wall (o.ä.), kommt aber immerhin eine Warnung wie “Statement has no effect”.

Um zu sehen, was Pointer tun, ist es manchmal nützlich, sich vorzustellen, was im Speicher vorgeht. Nach den Definitionen

char *sp;
char s[]="str";
int a=513;
int *ap=&a;
int **app=≈
sp = s;

könnte es im Speicher wie folgt aussehen:

Tatsächlich sind die Dinge komplizierter – erstens gibt es kaum noch Maschinen, auf denen Integers oder gar Pointer nur zwei Byte lang sind, zweitens würde der Speicher für str vermutlich ganz woanders liegen, und schließlich gibt es noch Fragen der Endianness.

Technik: Endianess

Auf manchen Maschinen wird 513 zur Bytefolge 2,1, auf anderen zur Bytefolge 1,2, je nach dem, ob das höherwertige Byte am Anfang oder am Ende gespeichert wird. Glücklicherweise muss man sich um diese Dinge meistens keine Sorgen machen, weil C einen davon in der Regel isoliert.

Wenn man allerdings binäre Information zwischen zwei verschiedenen Maschinen austauschen will, wird so etwas relevant (und es hilft auch, die Ausgabe von Programmen wie od zu verstehen).

Eine Maschine heißt big-endian, wenn das Byte mit der größten Potenz (most significant byte) im Speicher am Anfang der Zahl steht (“big end first”); die Zahl 0x1234abcd würde also als 0x12, 0x34, 0xab, 0xcd gespeichert, in einem Speicherabzug kann man also direkt die Zahlen lesen. Motorola- und Sun-Prozessoren speichern üblicherweise so, auch die diversen Internet-Standards mögen diese Darstellung, weshalb sie auch “network byte order” heißt.

Intel- und Alpha-Prozessoren hingegen speichern normalerweise little-endian, so dass das Byte mit der kleinsten Potenz hinten steht: 0xcd, 0xab, 0x34, 0x12. Vorteil dieser Darstellung ist, dass Ausdrücke wie *(char*)l eher das tun, was man wollen könnte (also: eine als long gespeicherte 12 bleibt auch über einen char pointer gelesen eine 12).

Wer damit spielen will, kann folgendes Programm auf verschiedenen Maschinen probieren (Intel, die RS/6000-Maschinen im URZ, VAXen, die ihr noch irgendwo auftreiben könnt):

#include <stdio.h>

int main(void)
{
  long l=0x1234abcdL;
  int i;
  unsigned char *cp=(unsigned char*)&l;

  for (i=0; i<4; i++) {
    printf("%x ", *cp++);
  }
  printf("\n");
  return 0;
}

Wie gesagt: Wenn man “brav” programmiert, sieht man nichts von der Endianness. Man muss sich aber mit ihr beschäftigen, wenn man z.B. Pointer verschiedener Größe aufeinander castet oder binär gespeicherte Ganzzahlen zwischen verschiedenen Maschinen austauscht, und in diese Kategorie fallen unter anderem auch UTF-16-kodierte Texte in Unicode (das mit der so genannten BOM auch eine Lösung des Problems anbietet – das führt hier allerdings zu weit). Ignoriert man Probleme dieser Art, erhält man unportable Programme (d.h. sie laufen nur auf Prozessoren einer bestimmten Endianess, und das will man in der Regel nicht).


Markus Demleitner

Copyright Notice