Teil 21 „Substrings“

Der C-Kurs
Antworten
Benutzeravatar
bodo
User
Beiträge: 321
Registriert: 14.02.2007, 17:21
Kontaktdaten:

Teil 21 „Substrings“

Beitrag von bodo » 02.01.2011, 14:38

C für BASIC-Programmierer

Arbeiten mit dem z88dk Cross-Compiler

von Jens Sommerfeld und Bodo Wenzel

Teil 21

Heute kommen wir endlich (?) zu dem C-Konzept, das als das Schwierigste gilt. Ich (Bodo) glaube das nicht, aber ich habe auch einen „Maschinensprache-Hintergrund“. Dieses Konzept heißt (gefährliche Musik im Hintergrund) ... ZEIGER!

Ach übrigens, bei Vickers heißt das Kapitel 21 „Substrings“, nur fürs Protokoll. Das passt ganz gut, denn um Zeichenketten zu bearbeiten kann man vorteilhaft Zeiger einsetzen.

Was ist nun ein Zeiger?

Das ist ganz einfach die Adresse eines Wertes!

Hm, das ist euch zu einfach? Gut: Der Wert eines Zeigers entspricht also der Adresse im Arbeitsspeicher, an der der Datenwert gespeichert ist, auf den der Zeiger zeigt. Die Adresse eines Bytes ist sozusagen die Hausnummer eines Bytes im Arbeitsspeicher. Wie, immer noch nicht klar? Vielleicht hilft dann dieses Bild:
21.png
21.png (4.32 KiB) 3281 mal betrachtet
Die Zahlen links bezeichnen die Adresse von Bytes im Arbeitsspeicher. Die Kästchen rechts sind die zugehörigen Bytes, von denen nur drei beispielhaft dargestellt sind. An der Adresse 16789 ist das Zeichen 'X' abgelegt. Ein Zeiger auf das 'X' hat dann den Zahlenwert 16789.

Nehmen wir an, die Variable „wert“ würde dort liegen, dann wäre dieser Zustand z.B. durch die folgenden C-Zeilen entstanden:

Code: Alles auswählen

    char wert;

    wert = 'X';
Die Adresse dieser Variablen bekommen wir als Wert, indem wir das Zeichen '&' vor den Variablennamen schreiben:

Code: Alles auswählen

    printf("ADRESSE VON WERT = %u\n", &wert);
Falls ihr ein Programm mit dieser Anweisung ausprobiert, wird sicher eine andere Adresse ausgegeben; das ändert aber nichts am grundsätzlichen Prinzip.

Worauf Zeiger zeigen

Zeiger können auf jeden beliebigen Datentyp zeigen.

Es gibt auch Zeiger auf Funktionen, bei denen geben wir aber nur den Namen der Funktion ohne die runden Klammern an:

Code: Alles auswählen

    printf("ADRESSE VON MAIN = %u\n", main);
Und schließlich gibt es den typenlosen Zeiger. Dieser enthält eine Adresse, an der ein typmäßig nicht definiertes Objekt liegt. Das kann also alles sein, ein Datenwert genauso wie der Befehl einer Funktion.

Zeigervariablen

Selbstverständlich nutzt uns das noch nicht soviel. Wir würden gern eine Variable haben, in der wir eine Adresse speichern können. Der Wert einer solchen Variablen ist also die Adresse einer anderen Variablen (Ganz spitzfindig kann der Zeiger auch auf sich selbst zeigen!).

Wir brauchen also einen Datentyp, der „Zeiger auf Datentyp“ bedeutet. Dies wird z.B. für ein Zeichen mit „char *“ ausgedrückt. Der Stern kennzeichnet die Eigenschaft „Zeiger auf...“. Ein Beispiel gefällig?

Code: Alles auswählen

    char zeichen;
    char *zeiger;

    zeiger = &zeichen; /* Jetzt zeigt "zeiger" auf "zeichen". */
Wie oben gesagt, können Zeiger auf jeden beliebigen Datentyp zeigen, sogar auf den „Nichtdatentyp“ void:

Code: Alles auswählen

int *z1;  /* Zeiger auf einen int */
long *z2; /* Zeiger auf einen long */
void *z3; /* Zeiger auf einen Wert eines beliebigen Typs */
int **z4; /* Zeiger auf einen Zeiger auf einen int */
Wegen der Prioritäten von Operatoren erfordert die Definition eines Zeigers auf eine Funktion die richtige Klammersetzung (Siehe dazu aber die Bemerkungen weiter unten! Das z88dk ist hier eingeschränkt.):

Code: Alles auswählen

void (*z5)(void);  /* Zeiger auf eine Funktion ohne Argumente,
                      die nichts zurückgibt */
long (*z6)(int p); /* Zeiger auf eine Funktion mit einem int-
                      Argument, die ein long zurückgibt */
int *z7(int p1, int p2);
  /* Dies ist dagegen ein Prototyp einer Funktion namens z7, die
   * zwei int-Argument bekommt, und die einen Zeiger auf einen int
   * zurückgibt!
   */
Als „Zeiger“ wird übrigens der Wert der Zeigervariablen bezeichnet, nicht die Variable selbst! Wenn ich den Wert der Variablen ändere, weise ich ihr einen anderen Zeiger zu; aber die Variable selbst bleibt die Variable.

Werte benutzen, auf die Zeiger zeigen

Jetzt können wir zwar Variablen anlegen und die Adresse von anderen Variablen darin speichern. Wie können wir aber jetzt auf den Wert zugreifen, auf den der Zeiger zeigt? Auch dazu wird der Stern verwendet, wir sprechen dann von „Dereferenzieren“:

Code: Alles auswählen

int zahl; /* Noch ist der Wert von "zahl" undefiniert. */
int *zeiger; /* Auch "zeiger" zeigt irgendwo(!) hin. */

zahl = 81; /* OK, damit hat "zahl" einen definierten Wert. */
zeiger = &zahl; /* Und "zeiger" zeigt jetzt auf "zahl". */

/* Lesender Zugriff über "zeiger":*/
printf("*ZEIGER = %d\n", *zeiger); /* Gibt "81" aus.*/

/* Schreibender Zugriff über "zeiger":*/
*zeiger = 23; /* Damit wird der Wert von "zahl" geändert */

printf("ZAHL = %d\n", zahl); /* Gibt "23" aus.*/
Zeigerliterale

Es gibt nur ein Literal (Ein Literal ist eine explizit angegeben Konstante, wie z.B. „23“ für eine Ganzzahl.) vom Typ „Zeiger“. Dies ist „NULL“. Meistens, aber nicht immer entspricht dieser Wert der Zahl 0. Mit diesem Wert können ungültige Zeigerwerte gekennzeichnet werden. Viele Systeme melden einen Fehler, wenn ein Programm einen solchen Zeiger dereferenzieren möchte.

Arithmetik mit Zeigern

Mit Zeigern können wir auch rechnen, allerdings nur sinnvolle Operationen ausführen:
  1. Durch die Addition einer Ganzzahl zu einem Zeiger wird er „erhöht“.
  2. Durch die Subtraktion einer Ganzzahl von einem Zeiger wird er „erniedrigt“.
  3. Durch die Subtraktion zweier Zeiger (gleichen Typs!) erhalten wir eine Ganzzahl.
Was bedeutet nun die Ganzzahl für einen Zeiger? Es ist die Anzahl an Datenwerten des Typs, auf den der Zeiger zeigt. Das ist ganz praktisch, weil dadurch die Addition einer „1“ den Zeiger auf den nächsten Wert im Speicher zeigen lässt. Für euch Assemblerfreaks: die Ganzzahl wird also mit der Größe des Datentyps multipliziert, bevor die Addition oder Subtraktion mit der Adresse erfolgt.

Ihr habt ja schon in Teil 7 Arrays von Zeichen angelegt. Im folgenden Beispiel verwenden wir ein Array von ints:

Code: Alles auswählen

    int feld[5];
    int *zeiger;

    zeiger = feld; /* "zeiger" zeigt auf das erste Element. */

    zeiger++; /* "zeiger" zeigt jetzt auf das zweite Element. */
Eigentlich hätten wir gern lieber „&feld[0]“ statt „feld“ geschrieben, weil das dem bisher Dargestellten entspricht. Durch Ausprobieren mit einem „richtigen“ Compiler könnt ihr sehen, dass „feld“, „feld + 0“ und „&feld[0]“ gleichwertige Ausdrücke sind, der Compiler des z88dk kann aber nur den ersten Ausdruck richtig auswerten; jedenfalls gibt er sonst eine entsprechende Warnung aus.

Umwandlung von Zeigern

Im Gegensatz zu den anderen Datentypen haben alle Zeiger immer dieselbe Größe in Bytes, denn es sind ja Adressen des Hauptspeichers (Ausnahmen bestätigen die Regel: manche Compiler für Mikrocontroller wie die 8051-Reihe kennen tatsächlich verschiedene Zeigertypen, die auch verschieden groß sind.). Und damit kann ein Zeiger auf einen Datentyp problemlos in einen Zeiger auf einen anderen Zeigertyp umgewandelt werden. Dies geschieht durch einen Cast, den ihr ja aus Teil 6 kennt:

Code: Alles auswählen

    char *zeichen_zeiger;
    int *ganzzahl_zeiger;

    ganzzahl_zeiger = (int *)zeichen_zeiger;
Ganz „universell“ ist natürlich der Zeigertyp „void *“, weil er den Datentyps des Speichers, auf den der Zeiger zeigt, überhaupt nicht festlegt.

Anwendung bei den Zeichenkettenfunktionen

Wie im Anfang schon angesprochen, sind Zeiger besonders gut zur Bearbeitung von Arrays geeignet. Arrays als solches lernen wir erst im nächsten Teil kennen, daher beschränken wir uns hier auf Zeichenketten.

Vom BASIC kennen wir das „Slicing“, also die Syntax mit TO und den Klammern. In C ist das nicht ganz so simpel, weil wir uns selbst um die Speicherplatzverwaltung kümmern müssen. Aber darüber hinaus gibt es glücklicherweise fertige Funktionen in der Standardbibliothek, die wir benutzen können:

Code: Alles auswählen

#include <stdio.h>
#include <string.h>

int main(void) {
    char *quell_string = "HALLO, WELT.";
    char ziel_string_1[5 + 1];
    char ziel_string_2[4 + 1];

    strncpy(ziel_string_1, quell_string + 0, 5);
    ziel_string_1[5] = '\0';
    strncpy(ziel_string_2, quell_string + 7, 4);
    ziel_string_2[4] = '\0';
    printf("\"%s\" - \"%s\"\n", ziel_string_1, ziel_string_2);

    return 0;
}
In diesem Programm könnt ihr gleich eine typische Anwendung eines Zeigers sehen: die Variable quell_string ist ein Zeiger auf eine Zeichenkette. Wir müssen nicht wissen, wo genau der Compiler sie hinlegt oder wieviele Zeichen es sind.

Im eigentlichen Programm verwenden wir die Funktion strncpy(), die als ersten Parameter den Zeiger auf das Ziel, als zweiten Parameter den Zeiger auf die Quelle, und als dritten Parameter die (maximale) Anzahl zu kopierender Zeichen bekommt. Weil diese Funktion aber nicht automatisch im Ziel ein Zeichenketten-Endezeichen anhängt, müssen wir das in der darauf folgenden Anweisung selbst machen.

Das Programm ist also äquivalent zu folgendem BASIC-Programm:

Code: Alles auswählen

10 Q$ = "HALLO, WELT."
20 Z1$ = Q$(1 TO 5)
30 Z2$ = Q$(8 TO 11)
40 PRINT """";Z1$;""" - """;Z2$;""""
Am Ende dieses Kapitels möchte ich euch noch beispielhaft drei Funktionen vorstellen, die bei der Zeichenkettenverarbeitung nützlich sind. Es gibt aber noch einige mehr, wie ein Blick in die Headerdatei „string.h“ zeigt.
  • char *strchr(char *, char): Die Funktion sucht im übergebenen String (erster Parameter) vom Anfang her nach dem angegebenen Zeichen (zweiter Parameter). Wenn es gefunden wird, gibt sie den Zeiger darauf zurück. Wenn es nicht gefunden wird, gibt sie den NULL-Zeiger zurück.
  • char *strrchr(char *, char): Die Funktion arbeitet wie strchr(), nur dass vom Ende her gesucht wird.
  • char *strstr(char *, char *): Die Funktion arbeitet so wie strchr(), nur sucht sie nach dem ersten Vorkommen des als zweiten Parameter übergebenen Strings.
Aufruf von Funktionen über Zeiger

Wenn wir einen Zeiger auf eine Funktion definieren können, müssten wir ja auch die Funktion aufrufen können, auf die dieser Zeiger zeigt. Und das ist tatsächlich möglich, und sieht auch ganz logisch aus:

Code: Alles auswählen

#include <stdio.h>

void funktion(void) {
    puts("IN FUNKTION");
}

int main(void) {
    void (*zeiger)();

    puts("VOR AUFRUF");
    zeiger = funktion;
    zeiger();
    puts("NACH AUFRUF");

    return 0;
}
Leider mag das z88dk nicht die eigentlich korrekte Definition des Zeigers, die da heißt:

Code: Alles auswählen

void (*zeiger)(void);
Analog zur üblichen REM-Zeile in BASIC, die ein Maschinenprogramm enthält, gibt es auch in C eine solche Möglichkeit. Dazu legen wir ein Array mit Zeichen an, in denen wir unser Maschinenprogramm ablegen. Im Beispiel ist das ganz einfach nur das Laden eines Wertes als Rückgabewert eines Unterprogramms, das dann aufgerufen wird:

Code: Alles auswählen

#include <stdio.h>

static unsigned char maschinencode[] = {
    0x21, 0x34, 0x12, /* LD HL,1234H */
    0xC9,             /* RET */
};

int main(void) {
    int (*funktionszeiger)(void);
    int ergebnis;

    funktionszeiger = maschinencode;
    ergebnis = funktionszeiger();
    printf("ERGEBNIS = %04x\n", ergebnis);

    return 0;
}
Aber das ist ein reichlich umständlicher Weg, zum einen wegen der Variablen und zum anderen wegen der manuellen Herstellung des Maschinencodes. In Teil 26 lernen wir, Assembler direkt in das C-Programm zu schreiben.

Und schließlich gibt es noch die Möglichkeit, eine beliebige Adresse als Funktion anzuspringen, entsprechend der USR-Funktion in BASIC. Die Verantwortung für den korrekten Aufruf trägt wie immer der Programmierer:

Code: Alles auswählen

    void (*zeiger)();

    zeiger = irgendein_wert;
    zeiger();
Mehrere Rückgabewerte

Dieser Teil ist schon wieder so lang geworden... daher bringen wir nur noch eine letzte Anwendung für Zeiger. Wir können sie gut einsetzen, wenn eine Funktion mehr als einen Wert zurückgeben soll.

Angenommen, wir möchten einen 16-Bit-Wert in seine 8-Bit-Hälften teilen. Das ginge beispielsweise so:

Code: Alles auswählen

#include <stdio.h>

void splitter(unsigned char *hi_zeiger, unsigned char *lo_zeiger,
    unsigned short wert) {
    *hi_zeiger = wert >> 8;
    *lo_zeiger = wert & 0xFF;
}

int main(void) {
    unsigned char hi;
    unsigned char lo;

    splitter(&hi, &lo, 0x5678);
    printf("HI = %02x, LO = %02x.\n", hi, lo);

    return 0;
}
Hausaufgaben
  1. Schreibt ein Programm, das einen NULL-Zeiger dereferenziert. Wie bekommt ihr heraus, worauf der Zeiger zeigt? Erklärt das Ergebnis.
  2. Was bedeutet das Ergebnis, wenn ihr zwei Zeiger voneinander abzieht? Zur Lösung dieser Aufgabe hilft sicher ein kleines Testprogramm:

    Code: Alles auswählen

    #include <stdio.h>
    
    int main(void) {
        int feld[5];
        int *zeiger1;
        int *zeiger2;
    
        zeiger1 = feld + 4;
        zeiger2 = feld;
        printf("ZEIGER1 = %u\n", zeiger1);
        printf("ZEIGER2 = %u\n", zeiger2);
        printf("DIFFERENZ = %u\n", zeiger1 – zeiger2);
    
        return 0;
    }
  3. Implementiert die Funktionen strchr(), strrchr() und strstr() selbst. Überprüft den Erfolg mit einem Testprogramm, das die Ergebnisse eurer Funktionen mit den Ergebnissen der fertigen Funktionen aus der Standardbibliothek vergleicht. Dazu müsst ihr eure Funktionen natürlich anders nennen, z.B. mit einem Präfix „mein_“. Kleiner Tipp: Denkt euch ein paar fiese Tests aus, bevor ihr eure eigenen Implementationen beginnt. Interessant sind vor allen Suchen nach den ersten oder letzten oder nicht vorhandenen Zeichen...
  4. Erklärt das Ergebnis folgenden Programms:

    Code: Alles auswählen

    #include <stdio.h>
    
    int main(void) {
        char zeichen1;
        char zeichen2;
        int *zeiger;
    
        zeichen1 = 0x12;
        zeichen2 = 0x34;
        zeiger = (int *)&zeichen2;
        printf("DEREFERENZIERTER WERT = %04x\n", *zeiger);
    
        return 0;
    }
B0D0: Real programmers do it in hex.

Antworten