10.12    Typqualifizierer bei Zeigern

Bei der Deklaration von Zeigern können Sie die Typqualifizierer const, volatile und restrict verwenden. Hier sollen einige gängige Anwendungen der Typqualifizierer in Bezug auf die Zeiger beschrieben werden.

10.12.1    Konstanter Zeiger

Da die zugewiesenen Adressen von konstanten Zeigern zur Laufzeit nicht mehr versetzt werden können, müssen diese schon bei der Definition mit einer Adresse initialisiert werden. Wohlgemerkt: Das Speicherobjekt, auf das der konstante Zeiger verweist, ist nicht konstant. Folgender Codeausschnitt soll dies demonstrieren:

01  int iarr[] = { 11, 22, 33 };  // int-Array
02 int* const c_ptr = &iarr[1]; // konstanter Zeiger
03 *c_ptr = 44; // iarr[1] erhält neuen Wert
04 c_ptr++; // Fehler!!!
05 *c_ptr = 66;

In Zeile (02) weisen Sie dem konstanten Zeiger die Adresse vom zweiten Element des Arrays iarr zu. Diese nun in c_ptr gespeicherte Adresse kann jetzt nicht mehr geändert werden. In Zeile (03) ändern Sie den Inhalt des zweiten Elements von iarr auf den Wert 44. In Zeile (04) wird versucht, den Zeiger auf das nächste Element im Array (hier iarr[2]) zu setzen, um diesen Wert dann in Zeile (05) zu ändern. Allerdings wird sich das Beispiel ab Zeile (04) nicht mehr übersetzen lassen, weil hier versucht wird, die zugewiesene Adresse des konstanten Zeigers zu ändern, sodass es zur Ausführung von Zeile (05) gar nicht mehr kommt – c_ptr ist hier ja ein konstanter Zeiger auf ein int.

10.12.2    Zeiger für konstante Daten

Benötigen Sie einen reinen Zeiger nur zum Lesen des Inhalts, auf den er verweist, müssen Sie einen Zeiger auf const verwenden. Damit ist nur noch das Lesen mit dem Indirektionsoperator auf das referenzierte Speicherobjekt möglich. Das Speicherobjekt, auf das der Zeiger verweist, muss hingegen nicht konstant sein. Ein kurzer Codeausschnitt soll einen solchen Zeiger demonstrieren:

01  int iarr[] = { 11, 22, 33 };  // int-Array
02 int const * c_ptr= &iarr[0]; // Zeiger zum Lesen auf Array
03 *c_ptr = 44; // Fehler!!!
04 // Folgendes ist erlaubt, da nur gelesen wird:
05 for(size_t i=0; i<sizeof(iarr)/sizeof(int); i++, c_ptr++){
06 printf("%d\n", *c_ptr);
07 }

In Zeile (02) legen Sie einen Zeiger auf konstante Array-Elemente fest. Sie können diesen Zeiger nicht verwenden, um den Inhalt der Array-Elemente zu ändern (wie es in Zeile (03) versucht wird). Ein lesender Zugriff, wie in den Zeilen (05) bis (07), ist hingegen jederzeit möglich. c_ptr ist hier ein Zeiger auf ein konstantes int.

10.12.3    Konstante Zeiger und Zeiger für konstante Daten

Neben den beiden eben erwähnten Möglichkeiten gibt es noch eine dritte Möglichkeit, bei der Sie weder die Adresse noch den Wert, auf den der Zeiger verweist, ändern können. Diese Form sieht folgendermaßen aus:

01  int iarr[] = { 11, 22, 33 };  // int-Array
02 int const * const c_ptr = &iarr[1];
03 *c_ptr = 44; // Fehler!!!
04 c_ptr++; // Fehler!!!
05 printf("%d\n", *c_ptr); // OK, da lesend

Durch die Verwendung von const vor und nach dem Zeiger verweist der Zeiger auf das zweite Element im Array iarr. Bei einem solchen Zeiger können Sie weder den Inhalt (wie in Zeile (03) zu sehen) noch die Adresse, auf die der Zeiger verweist (siehe Zeile (04)), ändern. Ein lesender Zugriff (wie in Zeile (05) zu sehen) ist nach wie vor möglich. Jetzt ist c_ptr ein konstanter Zeiger auf einen konstanten int.

10.12.4    Konstante Parameter für Funktionen

Der Qualifizierer const wird sehr gerne in Funktionen bei den formalen Parametern verwendet. Auch viele Standardfunktionen machen regen Gebrauch davon. Betrachten Sie beispielsweise die Syntax der Funktion printf():

int printf(const char* restrict format, ...);

Dank des Zeigers auf const ist es ausgeschlossen, dass innerhalb der Ausführung der Funktion printf() ein schreibender Zugriff auf format vorgenommen werden kann. Auf den Zeiger innerhalb der Funktion kann somit nur lesend zugegriffen werden.

10.12.5    restrict-Zeiger

Mit dem C99-Standard wurde der Typqualifizierer restrict neu eingeführt. Dieses Schlüsselwort qualifiziert sogenannte restrict-Zeiger. Der restrict-Zeiger hat eine enge Beziehung zu dem Speicherobjekt, auf das er verweist. Ein Beispiel wäre:

int* restrict iRptr = malloc (10 * sizeof (int) );

Damit schlagen Sie vor, dass Sie den von malloc() zurückgegebenen reservierten Speicher nur mit dem Zeiger iRptr verwenden. Wohlgemerkt: Mit dem Qualifizierer restrict geben Sie dem Compiler das Versprechen, dass Sie auf das Speicherobjekt ausschließlich mit diesem Zeiger zurückgreifen. Jede Manipulation außerhalb des restrict-Zeigers, und sei es nur lesend, ist unzulässig.

Es ist allerdings Ihre Aufgabe zu überprüfen, ob der restrict-Zeiger richtig verwendet wird und Sie nur über diesen Zeiger auf ein Speicherobjekt zugreifen. Der Compiler kann nicht überprüfen, ob Sie Ihr Versprechen eingehalten haben. Falls Sie die Regeln nicht einhalten, gibt es zwar keine Fehlermeldung des Compilers und häufig auch keine Probleme bei der Ausführung des Programms, aber dennoch ist das Verhalten laut Standard undefiniert.

Der Vorteil des restrict-Zeigers ist, dass Sie es dem Compiler ermöglichen, Optimierungen des Maschinencodes durchzuführen. Allerdings muss der Compiler diesem Hinweis nicht nachkommen und kann den Qualifizierer restrict auch ignorieren. Der restrict-Zeiger kann auch sehr gut bei Funktionen verwendet werden. Er zeigt dann an, dass sich zwei Zeiger in der Parameterliste nicht überlappen, sprich dasselbe Speicherobjekt verwenden dürfen. Beispielsweise ist bei

int cpy_number( int* v1, int* v2 ) {
/* ... */
}

nicht klar angegeben, ob sich die beiden Speicherobjekte, auf die die Zeiger v1 und v2 verweisen, überlappen dürfen oder nicht. Mit dem neuen Qualifizierer restrict ist dies jetzt sofort erkennbar:

int cpy_number( int* restrict v1, int* restrict v2 ) {
/* ... */
}

Wird trotzdem versucht, die Funktion mit sich überlappenden Speicherobjekten aufzurufen, ist das weitere Verhalten undefiniert. Ein ungültiger Aufruf kann beispielsweise wie folgt aussehen:

// Unzulässig wegen der restrict-Zeiger,
// zwei gleiche Speicherobjekte werden verwendet,
// die sich somit überlappen.
val = cpy_number( &x, &x );
// OK, zwei verschiedene Speicherobjekte
val = cpy_number( &x, &y );

Vom restrict-Zeiger wird seit C99 rege in der Standardbibliothek Gebrauch gemacht. Beispielsweise sieht die Syntax der Funktion strncpy() aus der Header-Datei string.h wie folgt aus:

#include <string.h>
char* strncpy(
char* restrict s1,
const char* restrict s2,
size_t n );

Die Funktion kopiert n Bytes vom Quell-Array s2 in das Ziel-Array s1. Da die beiden Zeiger als restrict deklariert sind, müssen Sie beim Aufruf der Funktion beachten, dass die Zeiger nicht auf dieselben Speicherobjekte verweisen, sich also nicht überlappen. Betrachten Sie dazu folgendes Beispiel:

char arr1[20];
char arr2[] = { "Hallo Welt" };
// Ok, 10 Zeichen von arr2 nach arr1 kopieren
strncpy( arr1, arr2, 10 );
arr1[10] = '\0'
// Unzulässig, Speicherbereiche überlappen sich.
strncpy( arr1, arr1, 5 );