B.9    Antworten und Lösungen zu Kapitel 10

  1. Einfache Zeiger erkennt man am Stern * in der Deklaration zwischen dem Datentyp und dem Bezeichner. Ein Zeiger wird dazu verwendet, Adressen als Wert zu speichern, um einen bestimmten Speicherbereich im Arbeitsspeicher zu referenzieren. Damit die Zeigerarithmetik auch richtig funktioniert, muss der Datentyp des Zeigers von demselben Typ wie der sein, auf dessen Adresse er verweist.

  2. Bei falscher Initialisierung von Zeigern, beispielsweise beim Vergessen des Adressoperators, kann es schnell passieren, dass ein Zeiger auf eine ungültige Adresse im Speicher verweist. Ein Zeiger kann quasi jede beliebige Adresse im Speicher referenzieren. Die meisten Betriebssysteme haben hier zwar einen Speicherschutz und lösen einen Speicherzugriffsfehler (segmentation fault) aus, aber dies muss nicht so sein, und der Standard schreibt diesbezüglich auch nichts vor. Keinerlei Speicherschutz gibt es oft z. B. bei Mikrocontrollern, denn hier kommt es vor allem auf die Ausführungsgeschwindigkeit von Steuerprogrammen und weniger auf die Sicherheit an.

  3. Mit einer Dereferenzierung können Sie direkt auf die Werte eines Speicherobjekts zugreifen, das Sie zuvor referenziert haben. Für den direkten Zugriff wird der Indirektionsoperator * vor den Bezeichner des Zeigers gestellt.

  4. Um zu vermeiden, dass ein Zeiger auf eine ungültige Adresse verweist, sollte man diesen gleich bei der Deklaration mit dem NULL-Zeiger initialisieren und vor jeder Verwendung eine Überprüfung auf NULL durchführen. Gibt die Überprüfung NULL zurück, hat der Zeiger noch keine gültige Adresse, und Sie können im Programm darauf reagieren. Globale Zeiger und Zeiger mit dem Schlüsselwort static werden automatisch mit dem NULL-Zeiger initialisiert. Allerdings beenden ältere Compiler, die noch den C99-Standard nicht vollständig implementiert haben, Zuweisungen von NULL an Zeiger mit der Fehlermeldung »null pointer assignment«.

  5. Der void-Zeiger ist ein typenloser Zeiger. Er wird vorwiegend verwendet, wenn der Datentyp eines Zeigers, der an eine Funktion übergeben werden soll, nicht von vornherein feststeht. Im Gegensatz zu einem typisierten Zeiger kann ein void-Zeiger nicht dereferenziert werden (bzw. nicht ohne ein explizites Typecasting dereferenziert werden).

  6. Steht const links vom Stern, handelt es sich um konstante Daten, also können die Daten in den Adressen, auf die der Zeiger zeigt, nicht geändert werden. Steht const hingegen auf der rechten Seite des Sterns, handelt es sich um einen konstanten Zeiger, und die Adresse, auf die der Zeiger zeigt, kann z. B. nicht geändert werden, indem man den konstanten Zeiger auf einen anderen Zeiger zeigen lässt.

  7. In Zeile (03) wurde der Adressoperator vor ival vergessen. Also müsste Zeile (03) richtig lauten:

    03  ptr = &ival;
  8. Beide Male wird der Wert 123456789 ausgegeben.

  9. Die Ausgabe des Codeausschnitts in diesem Beispiel lautet:

    56
    23
    3
    1
    0

    Dies möchten wir kurz erläutern: In Zeile (03) wird ptr1 die Anfangsadresse auf das erste Element von iarray übergeben. Diese Adresse wird in Zeile (05) um die Größe von zwei Elementen vom Typ int erhöht. Daher verweist der Zeiger ptr1 in diesem Fall auf das dritte Element in iarray. Das ist der Wert 56, wie er in Zeile (07) ausgegeben wird. ptr2 hingegen bekommt in Zeile (04) die Adresse des fünften Elements in iarray zugewiesen. In Zeile (06) wird die Adresse mit dem Inkrementoperator um die Größe von einem int erhöht, womit ptr2 jetzt auf die Adresse des sechsten Elements zeigt. In diesem Fall ist der Wert 23, wie die Ausgabe der Zeile (08) bestätigt. Dass in Zeile (09) die Subtraktion von ptr2-ptr1 den Wert 3 zurückgibt, liegt daran, dass die Anzahl der Elemente zwischen den beiden Adressen eben gleich drei ist. Das Umwandlungszeichen %td wird für den Rückgabetyp ptrdiff_t – also für die Subtraktion zweier Zeiger – benötigt. In Zeile (10) gibt der Ausdruck (ptr1 < ptr2) wahrheitsgemäß den Wert 1 zurück, weil die Adresse von ptr1 kleiner als die Adresse von ptr2 ist. Zur Demonstration wurde in Zeile (11) noch einmal das Gleiche gemacht. Hier wurde allerdings der Indirektionsoperator verwendet. Damit haben Sie die beiden Werte von ptr1 (= 56) und ptr2 (= 23) miteinander verglichen. Aus diesem Grund wird jetzt auch 0 zurückgegeben.

  10. Der Fehler in diesem Beispiel liegt darin, dass die Funktion den lokalen String buf aus Zeile (06) zurückgibt. Allerdings ist die Lebensdauer von lokalen Variablen in Funktionen nur zur Ausführungszeit der Funktion gültig. Nach dem Rücksprung aus der Funktion werden die Daten auf dem Stack wieder gelöscht, und str in der main()-Funktion verweist auf einen undefinierten Speicherbereich. Das Problem können Sie entweder lösen, indem Sie einen statischen Puffer mit dem Schlüsselwort static verwenden, einen globalen String benutzen oder mit malloc() einen genügend großen Speicherblock zur Laufzeit reservieren. Ein globaler String ist hier »unschön« (d. h. kein guter Programmierstil), und malloc() kennen Sie bisher noch nicht. Daher können Sie in Zeile (06) das Schlüsselwort static vor den String stellen. Dann klappt es auch mit der fehlerfreien Ausführung der Funktion:

    06    static char buf[MAX] = " ";
  11. Bei der ersten Version wird ein Zeiger erzeugt, der auf das Zeichen H in der konstanten Zeichenkette "Hallo Welt" zeigt. Die Zeichenkette "Hallo Welt" befindet sich wiederum irgendwo anders im Speicher. Mit der zweiten Version wird ein char-Array mit 11 Elementen erzeugt (das letzte Element ist \0).

  12. Im Folgenden sehen Sie eine solche Musterlösung, bei der, falls eine größere Länge als vorhandene Ziffern angegeben ist, vorne mit Nullen gefüllt wird. In der Praxis wäre allerdings ein dynamisch zur Laufzeit reservierter Speicher besser geeignet als ein Puffer, der hier mit static gekennzeichnet wurde.

    00  // kap010/loesung001.c
    01 #include <stdio.h>
    02 #include <stdlib.h>
    03 #include <string.h>
    04 #define MAX 255

    05 char *int2string(int number, int n) {
    06 static char buf[MAX];
    07 int i;
    08 if( n > MAX ) {
    09 return ("Fehler n > MAX");
    10 }
    11 for( i=0; i < n; i++ ) {
    12 buf[n-i-1] = (number % 10) + 48;
    13 number = number / 10;
    14 }
    15 buf[n] = '\0';
    16 return buf;
    17 }

    18 int main(void) {
    19 int val1 = 1234, val2 = 456789;
    20 char *ptr=NULL;
    21 ptr = int2string(val1, 4);
    22 printf("%s\n", ptr);
    23 ptr = int2string(val2, 9);
    24 printf("%s\n", ptr);
    25 return EXIT_SUCCESS;
    26 }

    Das Programm bei der Ausführung:

    1234
    000456789
  13. Hierzu eine Musterlösung:

    00  // kap010/loesung002.c
    01 #include <stdio.h>
    02 #include <stdlib.h>

    03 double Vquader(const double *a, const double *b,
    const double *c) {
    04 return ((*a) * (*b) * (*c));
    05 }

    06 int main(void) {
    07 double d1 = 6.0, d2=4.1, d3=3.2;
    08 printf("Quadervolumen: %lf\n", Vquader(&d1,&d2,&d3));
    09 return EXIT_SUCCESS;
    10 }
  14. Eine mögliche Musterlösung zur Aufgabe:

    00  // kap010/loesung003.c
    01 #include <stdio.h>
    02 #include <stdlib.h>
    03 #include <string.h>
    04 #define MAX 2048

    05 int main(void) {
    06 char buf[MAX]="";
    07 const char token[] = " ,.:!?\t\n";
    08 char *worte[MAX];

    09 printf("Bitte den Text eingeben: ");
    10 if( fgets(buf, MAX, stdin) == NULL ) {
    11 printf("Fehler bei der Eingabe\n");
    12 return EXIT_FAILURE;
    13 }
    14 char* ptr = strtok(buf, token);
    15 size_t i = 0;
    16 while(ptr != NULL) {
    17 worte[i] = ptr;
    18 ptr = strtok(NULL, token);
    19 i++;
    20 }
    21 printf("Zerlegt in einzelne Woerter:\n");
    22 for(size_t j=0; j < i; j++) {
    23 printf("%s\n", worte[j]);
    24 }
    25 return EXIT_SUCCESS;
    26 }

    Das Programm bei der Ausführung:

    Bitte den Text eingeben: Dieser Text wird in einzelne Woerter zerlegt
    Zerlegt in einzelne Woerter:
    Dieser
    Text
    wird
    in
    einzelne
    Woerter
    zerlegt