Tracks
/
C
C
/
Lehrplan
/
Zeiger
Ze

Zeiger in C

{one: "1 Übung", other: "%{count} Übungen"}

Über Zeiger

Ein Zeiger ist eine Variable oder Konstante, deren Wert die Adresse einer anderen Variable oder Konstante ist. Zeiger sind ein mächtiges und potenziell gefährliches Merkmal von C. Es kann schwierig sein, Zeiger zu verstehen.

int i = 5;

Wenn wir einer Variable einen Wert zuweisen, können wir danach zwei Fragen stellen.

Wer bist du?

Ich bin i.

Was hast du?

Ich habe die Ganzzahl 5.

Es gibt außerdem noch eine dritte Frage, die wir stellen können.

Wo bist du?

Ich bin an 0x6dfed4.

Moment, was?

Variablen und Konstanten speichern ihren Wert an einer Speicherstelle. Diese Stelle im Speicher wird als Adresse der Variable oder Konstante bezeichnet.

Der Adressoperator und der Indirektionsoperator (Teil 1)

Um die Adresse einer Variable zu erhalten, verwenden wir den Adressoperator, das ist &.

int i = 5;
int *i_address = &i;

Moment, was bedeutet dieses * vor i_address? Heißt das nicht multiplizieren?

Ein Typ, gefolgt von einem *, gefolgt von einem Bezeichner, ist die Art und Weise, wie man einen Zeiger auf eine Variable dieses Typs deklariert. Das * wird als Indirektionsoperator bezeichnet, weil es indirekt auf den Wert verweist, dessen Adresse der Zeiger enthält.

Okay, aber warum sollte man eine Variable anlegen, nur um die Adresse einer anderen Variable zu speichern?

Warum interessiert uns die Adresse einer Variable überhaupt?

Nun, es ist wichtig zu wissen, wo Variablen liegen, wenn sie sich in einem Array befinden.

Arrays und Zeiger

Wenn wir ein Array so anlegen

int my_ints[] = {1, 2};

dann ist der Wert von my_ints tatsächlich seine Adresse, die zufällig auch dieselbe ist wie die Adresse seines ersten Elements. Die Adresse seines zweiten Elements ist die Adresse des ersten Elements plus der Größe eines int auf dem System.

Um das besser zu sehen, führe Folgendes aus

#include <stdio.h>

int main() {
    // my_int's value is its address
    int my_ints[] = {1, 2};
    
    // since my_ints is already an address
    // we don't need the address operator to make a pointer from it.
    // a pointer to my_ints holds the address of my_ints
    int *my_ptr = my_ints;
    
    // the address of my_ints and the value of my_ptr are the same
    printf("my_ints is %p. my_ptr is %p.\n", my_ints, my_ptr);
    
    // the address of the first element is the same as the values in the line above
    printf("&my_ints[0] is %p. my_ptr + 0 is %p.\n", &my_ints[0], my_ptr + 0);
    
    // the address of the second element has increased by the size of an int
    printf("&my_ints[1] is %p. my_ptr + 1 is %p.\n", &my_ints[1], my_ptr + 1); // 
    
    return 0;
}

Zeigerarithmetik oder warum Array-Indizes bei 0 beginnen

Das vorherige Beispiel zeigt, warum die Array-Indizierung bei 0 statt bei 1 beginnt. Der Index in ein Array multipliziert den Index mit der Größe des Typs und addiert ihn zur Startadresse des Arrays. Da das erste Element mit dem Anfang des Arrays übereinstimmt, multiplizieren wir den Typ mit 0 und addieren das zur Startadresse, um das erste Element zu erhalten. Um das zweite Element zu erhalten, multiplizieren wir den Typ mit 1 und addieren das zur Startadresse. Wenn der Typ vier Bytes lang ist, liegt die Adresse des zweiten Elements vier Bytes höher als die des ersten Elements.

Das ist auch der Grund, warum Zeiger mit einem bestimmten Typ deklariert werden. Wenn ein Zeiger nicht wüsste, auf welchen Typ er zeigt, wüsste er nicht, um wie viele Bytes er voranschreiten muss, wenn man 1 zu ihm addiert. 1 zu einem Zeiger zu addieren bedeutet nicht unbedingt, dass wir ein Byte zur Adresse addieren. Es bedeutet, dass wir 1 mal die Länge seines Typs in Bytes addieren.

Aber halt, es kommt noch mehr! Du dachtest doch nicht, dass wir mit * fertig sind, oder?

Der Indirektionsoperator (Teil 2)

Jetzt, wo wir verstehen, wie Adressen mit der Array-Indizierung zusammenspielen, was nützt es uns, wenn wir den an einer Adresse gespeicherten Wert nicht bekommen? Wie machen wir das? Um den an einer Adresse gespeicherten Wert zu erhalten, verwenden wir den Indirektionsoperator, der, wie du dich vielleicht erinnerst, * ist. In dieser Situation wird der Indirektionsoperator manchmal auch als Dereferenzierungsoperator bezeichnet, weil er den Wert dereferenziert, dessen Adresse der Zeiger enthält. Wenn man ihn beim Dereferenzieren als Dereferenzierungsoperator bezeichnet, unterscheidet man ihn vom Indirektionsoperator zum Definieren eines Zeigers, aber technisch heißt * in beiden Verwendungen Indirektionsoperator.

Komm schon! Willst du mich auf den Arm nehmen? Wie viele Verwendungen hat *?! Multiplikation, einen Zeiger deklarieren und jetzt auch noch dereferenzieren! Das ist verwirrend! Und was genau ist Dereferenzieren eigentlich?

Ein Beispiel zeigt, wie man den Indirektionsoperator zum Dereferenzieren verwendet

#include <stdio.h>

int main() {
    // define the variable
    int my_ints[] = {1, 2};
    
    // define the pointer from the address of the variable
    int *my_ptr = my_ints;
    
    // prints: my_ints[0] is 1. *(my_ptr + 0) is 1.
    printf("my_ints[0] is %u. *(my_ptr + 0) is %u.\n", my_ints[0], *(my_ptr + 0));
    
    // prints: my_ints[1] is 2. *(my_ptr + 1) is 2.
    printf("my_ints[1] is %u. *(my_ptr + 1) is %u.\n", my_ints[1], *(my_ptr + 1));
    
    return 0;
}

Klammern können helfen, Mehrdeutigkeiten bei der Zeigerarithmetik zu vermeiden. Das folgende Beispiel gibt möglicherweise nicht aus, was der Programmierer beabsichtigt hat

#include <stdio.h>

int main() {
    int my_ints[] = {1, 42};
    int *my_ptr = my_ints;
    
    // prints: my_ints[0] is 1. *++my_ptr is 42.
    printf("my_ints[0] is %u. *++my_ptr is %u.\n", my_ints[0], *++my_ptr);
    
    // prints: my_ints[1] is 43. ++*my_ptr++ is 43.
    printf("my_ints[1] is %u. ++*my_ptr++ is %u.\n", my_ints[1], ++*my_ptr);

    return 0;
}

Sowohl * als auch ++ haben dieselbe Priorität, aber sie werden von rechts nach links ausgewertet. In der ersten Zeile wird my_ptr zuerst zum zweiten Element erhöht, das dann dereferenziert wird. In der zweiten Zeile wird my_ptr, das in der ersten Zeile zum zweiten Element erhöht wurde, zuerst dereferenziert, und dann wird der dereferenzierte Wert erhöht. Wenn wir diese Zeile zum obigen Beispiel hinzufügen

// prints: my_ints[1] is 43. *++my_ptr is (some garbage value).
printf("my_ints[1] is %u. *++my_ptr is %u.\n", my_ints[1], *++my_ptr);

In dieser Zeile haben wir den Zeiger über das Array hinaus erhöht. Wir haben das Array überschritten und zeigen auf die nächsten vier Bytes im Speicher, die der Zeiger als int interpretiert, aber tatsächlich könnten diese Bytes zu einer anderen Variable oder zu anderen Variablen gehören. Diese vier Bytes könnten Teil eines double sein oder vier chars. Das Problem ist, dass wir sie mit unserem Zeiger gar nicht ansehen sollten.

Wir hätten Folgendes verwenden können

// prints: my_ints[2] is (some garbage value).
printf("my_ints[2] is %u", my_ints[2]);

Da die Array-Indizierung mit einem Zeiger umgesetzt wird, gilt dieselbe Gefahr, das Array zu überschreiten.

Und da ein String ein Array von chars ist, kann man Zeiger auch mit Strings verwenden. Ein String wird oft mit Zeigersyntax deklariert, so wie hier

char *my_string = "This is my string";
// instead of
// char my_string[] = "This is my string";

Eine Frage noch. Warum setzen wir das Sternchen neben den Bezeichner? Warum nicht neben den Typ, schließlich ist es ein Zeiger auf diesen Typ? Weil dies

int* my_ptr1, my_ptr2_that_is_really_an_int;

keine zwei Zeiger auf ein int deklariert, sondern einen Zeiger auf ein int und ein int. Zwei Zeiger auf ein int deklariert man so

int *my_ptr1, *my_ptr2;

Es ist also gut, das * nah am Bezeichner zu platzieren, um Mehrdeutigkeit zu minimieren. Oder deklariere nur einen Bezeichner pro Zeile. Und wenn du noch nicht genug von * hast, willkommen bei...

Zeiger auf Zeiger

Arrays von Strings werden häufig so dargestellt

#include <stdio.h>

void print_strings(char **passed_in_strings) {
    printf("%s\n", *passed_in_strings);
    printf("%s\n", *(passed_in_strings + 1));
}

int main() {
    char *my_strings[] = {"One string", "Two string"};
    print_strings(my_strings);
    return 0;
}

Es ist vielleicht nicht offensichtlich, aber das String-Array wird als Zeiger auf ein Array von char-Arrays deklariert und initialisiert. Wenn das Array von char-Arrays an print_strings() übergeben wird, beschreibt die Signatur es als Zeiger auf Zeiger auf char.

Woher wissen wir also, dass char ** wirklich ein Zeiger auf ein Array von Zeigern auf char-Arrays ist und nicht ein Zeiger auf einen Zeiger auf ein einzelnes char?

Wissen wir nicht.

Dies ist eines der vielen Dinge, die die Verwendung von Zeigern verwirrend machen können. Das Folgende lässt sich möglicherweise kompilieren

#include <stdio.h>

void print_strings(char **passed_in_strings) {
    // prints: A (followed by gibberish)
    printf("%s\n", *passed_in_strings);
    // prints gibberish
    printf("%s\n", *(passed_in_strings + 1));
}

int main() {
    char my_char = 'A';
    char *my_ptr = &my_char;
    char **my_ptr_to_ptr = &my_ptr;
    print_strings(my_ptr_to_ptr);
    return 0;
}

Aber print_strings() wird nicht wie beabsichtigt verwendet. Eine andere Signatur hilft nicht. Ändert man die Signatur so

void print_strings(char *passed_in_strings[]) {
    // code snipped
}

lässt sie sich möglicherweise weiterhin kompilieren und gibt weiterhin Kauderwelsch aus. Aber es hat wirklich keinen Nutzen, einen Zeiger auf einen Zeiger auf ein einzelnes char zu übergeben. In der Regel steht char **, wenn du es siehst, für einen Zeiger auf ein Array von Zeigern auf char-Arrays.

Wir haben gesehen, wie ein Zeiger an eine Funktion übergeben wird. Aber warum sollte man einen Zeiger an eine Funktion übergeben?

Einen Zeiger an eine Funktion übergeben

In C werden Argumente per Wert an Funktionen übergeben. Das bedeutet, dass der Wert aus dem Argument kopiert wird und diese Kopie lokal in der Funktion ist. Wenn die Funktion endet, endet auch der kopierte Wert.

Sieh, was passiert, wenn wir Werte innerhalb einer Funktion ändern wollen

#include <stdio.h>

void swap(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
    //prints:  In 'swap', a is 4 and b is 3.
    printf("In 'swap', a is %d and b is %d.\n", a, b);
}

int main() {
    int a = 3, b = 4;
    swap(a, b);
    // prints: After 'swap', a is 3 and b is 4
    printf("After 'swap', a is %d and b is %d.\n", a, b);
    return 0;
}

Das liegt daran, dass a und b in swap(), obwohl die Variablen dieselben Namen haben, nicht dieselben a und b in main() sind. Sie sind Kopien der Werte von a und b. Können wir swap ändern, damit es wie beabsichtigt funktioniert?

Ja, können wir! Wir können die Adressen von a und b als Zeiger übergeben. Aber was nützt das, wenn die Funktion die Zeiger kopiert? Es nützt etwas, weil eine kopierte Adresse immer noch dieselbe Adresse ist. Durch das Dereferenzieren der Adresse erhalten wir Zugriff auf den ursprünglichen Wert, so wie er in main() definiert wurde.

#include <stdio.h>

// pass in pointers to a and b
void swap(int *a, int *b) {
    // set temp to value at a's dereferenced address
    int temp = *a;

    // set a's value to value at b's dereferenced address
    *a = *b;

    // set b's value to temp
    *b = temp;

    // prints: In 'swap', a is 4 and b is 3.
    printf("In 'swap', a is %d and b is %d.\n", *a, *b);
}

int main() {
    int a = 3, b = 4;
    swap(&a, &b);
    // prints: After 'swap', a is 4 and b is 3.
    printf("After 'swap', a is %d and b is %d.\n", a, b);
    return 0;
}

Das sind jetzt viele * in swap, aber jetzt funktioniert es wie beabsichtigt.

Eine Möglichkeit, den Indirektionsoperator zu lesen, ist „Wert an“. Also ist *a der Wert an a. Wie gut funktioniert das mit Strings?

Zeiger auf char

Wir wissen, dass der Wert einer Array-Variable die Adresse ist, an der das Array beginnt. Und wir wissen, dass wir einen String entweder als char-Array, char my_string[], oder als Zeiger auf char, char *, definieren können. Worin unterscheiden sie sich?

In diesem Code übergeben wir ein char *-Argument an describe_me(). Das Argument adjective ist als char-Array definiert und wird von der Funktion als Zeiger auf char akzeptiert.

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

// takes pointer to char
void describe_me(char *adjective){
    strcpy(adjective, "evil");
    // prints: I am evil.
    printf("I am %s.\n", adjective);
}

int main() {
    // defined as a char array
    char adjective[] = "good";
    describe_me(adjective);
    // prints: I am really evil.
    printf("I am really %s.\n", adjective);
    
    return 0;
}

Wir können adjective dauerhaft ändern, was zeigt, dass das definierte char-Array als Zeiger auf char akzeptiert werden kann.

Versuchen wir nun, adjective direkt als Zeiger auf char zu definieren.

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

void describe_me(char *adjective){
    // segmentation fault on this line when program is executed
    strcpy(adjective, "evil");
    printf("I am %s.\n", adjective);
}

int main() {
    char *adjective = "good";
    describe_me(adjective);
    printf("I am really %s.\n", adjective);
    
    return 0;
}

Erinnerst du dich, dass Zeiger mächtig, aber auch potenziell gefährlich sind? Aber warum funktioniert es nicht? Ein Array ist ein Zeiger, und ein Zeiger hat mit einem char-Array funktioniert! Der Grund ist, dass adjective vorher als char-Array definiert war, also als eine Variable, die zu main() gehört. Jetzt ist adjective als String-Literal definiert, was bedeutet, dass die charss in schreibgeschützten statischen Speicher geschrieben werden, der zum gesamten Programm gehört. Beide sind Zeiger, aber sie zeigen auf unterschiedliche Dinge. Zu versuchen, einen String-Literalwert zu ändern, ist wie der Versuch, den Literalwert von 5 zu ändern. Das ist nicht erlaubt. Das Problem ist also nicht wirklich, dass es ein Zeiger ist. Das Problem ist, worauf er zeigt.

Über GitHub bearbeiten Der Link öffnet sich in einem neuen Fenster oder Tab