Zeiger und Speicheradressen in C

Lerne, wie ein C-Programm seine Variablen im Speicher anordnet und wie du mit Zeigern direkt mit Speicheradressen arbeitest. Ermittle Adressen mit &, lies und ändere Werte über Zeiger und übergib Variablen als Referenz an Funktionen. Durchlaufe Arrays und Zeichenketten mit Zeigerarithmetik, verwende Zeiger auf Zeiger für Kommandozeilenargumente und für Funktionen, die einen Zeiger des Aufrufers versetzen, und rufe Funktionen über Funktionszeiger auf, darunter Vergleichsfunktionen für das standardmäßige qsort.

AnfängerEinheit 4 von 8 in Kurs: C-Programmierung – Zeiger, Speicherverwaltung und sicheres C

Was du lernst

  1. Veranschauliche dir, wie die Variablen eines C-Programms Bytes im Speicher belegen, speichere Adressen in Zeigervariablen und lies und ändere damit die Variablen, auf die sie zeigen.
    • Die Anordnung von Variablen im Speicher veranschaulichen
      Beschreibe Speicher als nummerierte Bytes, ermittle Größe und Adresse von Variablen und Arrayelementen und erkläre, wie lange die einzelnen Arten von Variablen existieren.
    • Zeiger deklarieren und Adressen mit & ermitteln
      Deklariere Zeigervariablen des richtigen Typs, speichere die Adresse einer Variablen mit dem Adressoperator und gib einen Zeiger aus, richte ihn neu aus oder setze ihn sicher zurück.
    • Werte über Zeiger lesen und ändern
      Verwende den Dereferenzierungsoperator, um das Objekt zu lesen und zu ändern, auf das ein Zeiger zeigt. Verfolge Code anhand von Speicherdiagrammen und vermeide die Dereferenzierung ungültiger Zeiger.
  2. Schreibe Funktionen, die über Zeigerparameter Variablen ihres Aufrufers ändern, schütze Daten, die nur gelesen werden sollen, mit const und gib Zeiger zurück, die gültig bleiben.
    • Variablen über Zeigerparameter als Referenz übergeben
      Erkläre, warum C-Funktionen Kopien ihrer Argumente erhalten, und verwende Zeigerparameter, um Variablen eines Aufrufers zu ändern, Werte zu vertauschen, mehrere Ergebnisse zurückzugeben und Eingaben mit scanf zu lesen.
    • Daten mit const schützen und Zeiger sicher zurückgeben
      Kennzeichne Daten, die nur gelesen werden sollen, mit const, unterscheide einen Zeiger auf konstante Daten von einem konstanten Zeiger und gib Zeiger zurück, die nach dem Ende der Funktion gültig bleiben.
  3. Bewege Zeiger durch Arrays und Zeichenketten, erkläre den Zusammenhang zwischen Arraynamen und Zeigern und halte jeden Zeiger innerhalb des Arrays, zu dem er gehört.
    • Mit Zeigern rechnen
      Addiere Ganzzahlen zu Zeigern, subtrahiere und vergleiche Zeiger in dasselbe Array und erkenne, welche Ergebnisse gültig sind.
    • Den Zusammenhang zwischen Arrays und Zeigern erklären
      Nutze die Konvertierung eines Arraynamens in einen Zeiger und die Gleichheit von a[i] und *(a + i) und erkenne die tatsächlichen Unterschiede zwischen einem Array und einem Zeiger.
    • Arrays und Zeichenketten mit Zeigern durchlaufen
      Schreibe Schleifen, die Arrays und nullterminierte Zeichenketten mit Zeigern durchlaufen, verwende gebräuchliche Ausdrücke wie *p++ korrekt und halte die Grenzen ein.
  4. Verfolge mehrere Indirektionsebenen, lass Funktionen einen Zeiger des Aufrufers aktualisieren und verwende Zeigerarrays wie die Kommandozeilenargumente.
    • Zeiger auf Zeiger verwenden
      Deklariere und dereferenziere einen Zeiger auf einen Zeiger, verfolge ihn in einem Speicherdiagramm und ermögliche damit einer Funktion, einen Zeiger des Aufrufers zu ändern.
    • Zeigerarrays und Kommandozeilenargumente verwenden
      Erstelle Arrays von Zeichenkettenzeigern, verarbeite argc und argv als Zeiger und unterscheide ein Zeigerarray von einem zweidimensionalen Array.
  5. Speichere und rufe Funktionen über Zeiger auf, übergib Funktionen an andere Funktionen und sortiere Daten beliebigen Typs mit dem standardmäßigen qsort und einer Vergleichsfunktion als Rückruffunktion.
    • Funktionszeiger deklarieren, ihnen Werte zuweisen und Funktionen über sie aufrufen
      Schreibe und lies Funktionszeigerdeklarationen, rufe Funktionen über sie auf und erstelle Funktionstabellen, die das Verhalten zur Laufzeit auswählen.
    • Rückruffunktionen schreiben und mit qsort sortieren
      Übergib Funktionen als Argumente, verarbeite Daten beliebigen Typs mit void-Zeigern und schreibe korrekte Vergleichsfunktionen für qsort.

Zeiger und Speicheradressen in C einfach erklärt

5 Kapitel

1

Speicher, Adressen und referenzierte Werte

Eine Variable hat einen Wert, belegt Speicher und hat eine Lebensdauer. Ein Zeiger ermöglicht den Zugriff auf diese Variable über ihre Adresse. Beim Lesen von Code verfolgst du daher sowohl die im Zeiger enthaltene Adresse als auch den dort vorgefundenen Wert. Beginne mit der Anordnung der Bytes und verfolge dann, was geschieht, wenn du eine Adresse ermittelst, einen Zeiger neu ausrichtest und über ihn schreibst. Kleine Speicherdiagramme machen diese unterschiedlichen Operationen sichtbar, bevor sie in Funktionen und Schleifen auftreten.

1.1 Bytes, Größen und Lebensdauer

Du kannst dir Speicher als Folge nummerierter Bytes vorstellen. Jede Nummer ist eine Adresse. Ein Objekt wie ein int belegt aufeinanderfolgende Bytes, und seine Adresse bezeichnet sein erstes Byte. Der Name im Quellcode ermöglicht dem Compiler, dieses Objekt zu finden; die Sprache verlangt nicht, dass neben dem Wert eine Namenskennzeichnung gespeichert wird.

Speicher mit sizeof messen

sizeof gibt die Größe in Bytes als Wert vom Typ size_t an. Verwende %zu zur Ausgabe:

int scores[4] = {12, 18, 15, 21};
printf("%zu %zu\n", sizeof scores, sizeof scores[0]);

Bei int mit 4 Byte gibt dies 16 4 aus. Das erste Ergebnis misst alle vier Elemente, das zweite eines. sizeof(char) ist immer 1. Eine verbreitete Anordnung auf 64-Bit-Desktops verwendet int mit 4 Byte, double mit 8 Byte und Objektzeiger mit 8 Byte. Andere Zielsysteme können abweichen. Selbst unter 64-Bit-Systemen belegt long unter Linux und macOS häufig 8 Byte, unter Windows aber 4.

Speichergröße und Textlänge beantworten unterschiedliche Fragen. Bei char word[20] = "hi"; ist sizeof word gleich 20, während strlen(word) gleich 2 ist. Das Array enthält weiterhin 20 Elemente, obwohl seine Zeichenkette nur zwei Buchstaben und ein abschließendes Nullzeichen verwendet. Außer bei Arrays variabler Länge stehen diese Objektgrößen zur Kompilierzeit fest.

Die Adressen eines Arrays verfolgen

Hexadezimaladressen verwenden die Ziffern 0 bis 9, danach a bis f. Das Präfix 0x kennzeichnet die Basis 16. Bei einem Beispielarray, das bei 0x1000 beginnt, beginnen vier Elemente mit je 4 Byte hier:

ElementErstes ByteLetztes Byte
scores[0]0x10000x1003
scores[1]0x10040x1007
scores[2]0x10080x100B
scores[3]0x100C0x100F

0x100C ist dezimal 4.108 und liegt zwölf Byte hinter 0x1000. Zwischen Arrayelementen gibt es keine Lücken. Für getrennt deklarierte Variablen sind weder Reihenfolge noch Abstand garantiert; tatsächlich ausgegebene Adressen können sich auch zwischen Programmläufen ändern. Ein Compiler kann einen Wert in einem Prozessorregister halten, wenn kein adressierbarer Speicher benötigt wird.

Speicherausrichtung und Byte-Reihenfolge

Die Speicherausrichtung schränkt ein, wo Objekte eines Typs beginnen dürfen. Bei einer verbreiteten Anordnung liegen int mit 4 Byte auf Vielfachen von 4 und double mit 8 Byte auf Vielfachen von 8. Der Compiler sorgt bei deklarierten Objekten für eine geeignete Speicherausrichtung.

Die Byte-Reihenfolge ist die Reihenfolge der Bytes innerhalb eines Werts aus mehreren Bytes. Bei einem unsigned int mit 4 Byte, der 0x12345678 enthält, stehen bei Little-Endian-Speicherung die Bytes 78 56 34 12 in aufsteigender Adressreihenfolge; bei Big-Endian-Speicherung sind es 12 34 56 78. Ein unsigned char * kann die Repräsentation eines Objekts byteweise untersuchen. Die Byte-Reihenfolge ändert nicht die Indexreihenfolge des Arrays.

Das Objekt am Leben halten

Eine gewöhnliche lokale Variable hat automatische Speicherdauer: Ihre Lebensdauer erstreckt sich über die Ausführung ihres Blocks. Typische Implementierungen halten solche Objekte im Stackframe eines Aufrufs und verwenden diesen Speicher anschließend wieder. Globale Variablen, static-Variablen und Zeichenkettenliterale existieren während des gesamten Programms. Das Speichern einer Adresse verlängert die Lebensdauer ihres Objekts nicht.

Wichtige Erkenntnisse

  • sizeof misst Bytes; strlen zählt die Zeichen vor dem abschließenden Nullzeichen einer Zeichenkette.
  • Arrayelemente belegen in Indexreihenfolge aufeinanderfolgenden Speicher.
  • Für getrennte Variablen sind die relativen Adressen nicht garantiert.
  • Die Speicherausrichtung bestimmt geeignete Anfangsadressen; die Byte-Reihenfolge bestimmt die Reihenfolge der Bytes innerhalb eines Werts.
  • Eine gespeicherte Adresse bleibt nur nutzbar, solange ihr Objekt existiert.

1.2 Eine Adresse speichern und ausgeben

Ein Zeiger speichert eine Adresse und hat einen Typ, der das Objekt beschreibt, auf das er zugreifen kann. Die Deklaration eines int * stellt Speicher für einen Zeiger bereit; sie erstellt keinen int zur Verwendung durch diesen Zeiger. Gib dem Zeiger bei seiner Deklaration einen bekannten Wert.

Die Adresse eines Objekts ermitteln
int count = 42;
int *p = &count;

Der unäre Adressoperator & ermittelt die Adresse von count. Beginnt count bei der Beispieladresse 0x1000, enthält p den Wert 0x1000, während count den Wert 42 enthält. Der Zeiger ist ebenfalls ein Objekt: &p ist die Adresse des separaten Speicherbereichs, der diesen Zeiger enthält.

Du kannst die Adresse eines Arrayelements ermitteln, etwa mit &scores[2]. Eine numerische Konstante oder ein Berechnungsergebnis ist kein gespeichertes Objekt, das du auf diese Weise adressieren kannst: &42 und &(count + 1) sind ungültig. Der binäre Ausdruck a & b verwendet dasselbe Symbol für ein bitweises UND; an der Anzahl der Operanden erkennst du die jeweilige Operation.

Jeden Deklarator lesen
DeklarationBedeutung
int *p, q;Ein Zeiger und ein gewöhnlicher int
int *p, *q;Zwei Zeiger auf int
double *r;Ein Zeiger auf double

Leerraumzeichen ändern keinen dieser Typen. int* p und int *p sind gleichwertig. Deklarierst du pro Zeile einen Zeiger, übersiehst du sein Sternchen weniger leicht. Auf verbreiteten 64-Bit-Desktops belegt ein Objektzeiger 8 Byte, doch seine eigene Größe sagt nichts über die Größe des Objekts aus, auf das er zeigt.

Der Typ, auf den der Zeiger zeigt, ist entscheidend. Verwende für double price = 9.5; den Zeiger double *r = &price;. Die Initialisierung eines int * mit &price erzeugt eine Meldung über inkompatible Zeigertypen. Eine explizite Typumwandlung, die diese Meldung verbirgt, macht einen int-Zugriff auf diesen double nicht gültig: Der Typ bestimmt, wie auf die gespeicherten Bytes zugegriffen wird.

Einen Zeiger neu ausrichten
int a = 5, b = 8;
int *p = &a;
p = &b;

Die letzte Zuweisung ersetzt die Adresse in p. Sie verschiebt keine der beiden Ganzzahlen und kopiert auch nicht den Wert einer Ganzzahl in die andere. Nun zeigt p auf b; a ist weiterhin 5 und b weiterhin 8. Mehrere Zeigervariablen können auch dieselbe Adresse speichern.

Einen bekannten leeren Zustand verwenden

Ein nicht initialisierter lokaler Zeiger hat einen unbestimmten Wert und wird manchmal wilder Zeiger genannt. Untersuche und dereferenziere ihn nicht. Initialisiere ihn mit der Adresse eines gültigen Objekts oder mit NULL, das unter anderem über die Headerdateien <stddef.h> und <stdio.h> verfügbar ist:

int *pending = NULL;
if (pending == NULL) {
    pending = &count;
}

Ein Nullzeiger bezeichnet kein Objekt. !pending ist eine weitere Prüfung auf einen Nullzeiger. Diese Prüfungen sind nur für initialisierte Zeiger sinnvoll; sie überprüfen weder eine beliebige Adresse noch einen Zeiger, dessen Objekt nicht mehr existiert.

Einen Objektzeiger ausgeben
printf("%p\n", (void *)p);

%p erwartet void *, daher verwende bei einem typisierten Objektzeiger die explizite Typumwandlung. Die Implementierung legt die ausgegebene Darstellung fest; häufig ist sie hexadezimal. %d und %x erwarten Ganzzahlargumente bestimmter Typen und können %p nicht ersetzen.

Wichtige Erkenntnisse

  • Initialisiere einen Zeiger mit einer gültigen Adresse oder NULL.
  • &count ist eine Adresse; count ist die gespeicherte Ganzzahl.
  • Jeder Zeigername braucht in einer Deklaration sein eigenes *.
  • Zeigertypen müssen zu den Objekten passen, auf die über sie zugegriffen wird.
  • p = &b ändert den Zeiger und lässt die Ganzzahlen unverändert.
  • Gib einen Objektzeiger mit %p und einem Argument vom Typ void * aus.

1.3 Einem Zeiger zum Lesen oder Schreiben folgen

Der unäre Dereferenzierungsoperator * folgt einem Zeiger zu seinem Objekt. Zeigt p auf x, bezeichnet *p das Objekt x selbst. Das Lesen von *p liest dieses Objekt; eine Zuweisung an *p ändert es.

Einen Schreibzugriff über eine Adresse verfolgen
int x = 10;
int *p = &x;
*p = *p + 5;
int y = *p * 2;
printf("%d %d %d\n", x, *p, y);

Zuerst speichert p die Adresse von x. Auf der rechten Seite der Zuweisung liest *p den Wert 10. Die Addition von 5 ergibt 15, und das *p auf der linken Seite speichert 15 in x. Die nächste Zeile multipliziert den aktualisierten Wert mit 2, sodass y zu 30 wird. Die Ausgabe ist 15 15 30.

Der Kontext unterscheidet die Sternchen: int *p deklariert einen Zeiger, das unäre *p greift auf ein Objekt zu, und *p * 2 enthält außerdem eine binäre Multiplikation. Für ein gültiges Objekt x bezeichnet *&x das Objekt x; für einen Zeiger auf ein gültiges Objekt liefert &*p den Zeigerwert zurück.

Adressen und Inhalte auseinanderhalten

Beginne jede Ablaufverfolgung mit zwei Angaben: welche Adresse jeder Zeiger enthält und welchen Wert jede Ganzzahl enthält. Angenommen, p zeigt auf a mit dem Wert 3 und q auf b mit dem Wert 8.

Anweisung ausgehend von diesem ZustandWirkung
*p = *q;Kopiert 8 nach a; die Zeiger behalten ihre Ziele
p = q;Lässt p auf b zeigen; die Ganzzahlen behalten ihre Werte

Ein Speicherdiagramm verwendet ein Kästchen für jedes Objekt. Beschrifte ein Kästchen mit Name und Adresse, trage seinen aktuellen Wert ein und zeichne einen Pfeil vom Zeiger zu seinem Ziel. Eine Adresszuweisung versetzt einen Pfeil; eine Zuweisung über einen Zeiger ändert den Inhalt eines Zielkästchens.

Nach p = q sind die Zeiger Aliase für b. Führst du anschließend *p = 20; aus, liefert das Lesen von *q oder b den Wert 20. Das Kopieren einer Adresse schafft einen weiteren Zugriffsweg auf dasselbe Objekt, keine weitere Ganzzahl.

Prüfen, ob eine Dereferenzierung gültig ist

Ein für einen Zugriff verwendeter Zeiger muss ein noch existierendes Objekt eines geeigneten Typs bezeichnen; zum Schreiben muss das Objekt außerdem veränderbar sein. Nullzeiger bezeichnen kein Objekt. Nicht initialisierte Zeiger haben kein bekanntes nutzbares Ziel. Ein Zeiger wird zum hängenden Zeiger, wenn die Lebensdauer seines Ziels endet.

Der Zugriff über einen ungültigen Zeiger ist undefiniertes Verhalten: C stellt keine Anforderungen an das Ergebnis. Das Programm kann mit einer Speicherzugriffsverletzung stoppen, Daten beschädigen oder scheinbar normal laufen. Ein scheinbar erfolgreicher Lauf kann nicht belegen, dass der Zugriff gültig war.

Prüfe einen Zeiger, der zulässigerweise ein Nullzeiger sein kann, vor dem Zugriff:

if (p != NULL) {
    *p = 0;
}

Weist du einem nicht mehr benötigten Zeiger NULL zu, kannst du eine spätere versehentliche Verwendung dieser Variablen verhindern. Andere Aliase werden dadurch nicht zurückgesetzt. Behalte die Lebensdauer des Objekts gesondert im Blick.

Den gemeinten Wert in Klammern setzen

Das Postfix-++ bindet stärker als das unäre *. Daher bedeutet *p++ dasselbe wie *(p++): Der alte Zeiger wird verwendet, um einen Wert zu erreichen, und der Zeiger wird weitergesetzt. (*p)++ erhöht den Wert, auf den der Zeiger zeigt, und lässt den Zeiger an seiner Stelle. *p + 1 liest den Wert und addiert 1, ohne das Ergebnis zu speichern. Das sind drei unterschiedliche Aktionen.

Wichtige Erkenntnisse

  • *p bezeichnet das über p erreichte Objekt.
  • Das Kopieren einer Adresse erzeugt Aliase; das Kopieren von *p kopiert einen Wert.
  • Eine Dereferenzierung setzt ein noch existierendes Objekt und einen geeigneten Zugriffstyp voraus.
  • Bei undefiniertem Verhalten ist kein Ergebnis zugesichert, auch kein Absturz.
  • *p++ setzt den Zeiger weiter; (*p)++ erhöht seinen Zielwert.
2

Funktionen mit Daten des Aufrufers arbeiten lassen

Eine Funktion erhält von jedem Argument eine Kopie, auch von einem Zeigerargument. Über die kopierte Adresse lässt sich weiterhin ein Objekt des Aufrufers erreichen. So werden das Vertauschen von Werten, Eingabefunktionen und mehrere Ausgaben möglich. Die Funktionsschnittstelle sollte außerdem zeigen, ob sie die Daten ändert. Verfolge die Adresse in den Aufruf hinein, unterscheide lokale Änderungen von Schreibzugriffen über die Adresse und prüfe, welche Objekte noch existieren, wenn ein zurückgegebener Zeiger verwendet wird.

2.1 Variablen des Aufrufers über Parameter ändern

C übergibt jedes Argument als Wert. Ein Parameter ist eine lokale Variable, die aus dem Argument initialisiert wird. Die Übergabe einer Adresse ändert diese Regel nicht: Die Funktion erhält eine Kopie der Adresse, und diese Kopie kann das Objekt des Aufrufers erreichen.

Zwei Reset-Funktionen vergleichen
void reset_copy(int n) {
    n = 0;
}

void reset(int *n) {
    *n = 0;
}

Bei int score = 12; ändert reset_copy(score) nur das lokale n. score bleibt 12. Der Aufruf reset(&score) kopiert die Adresse von score in den Zeigerparameter n; die Zuweisung an *n ändert dann score zu 0. Diese Technik wird häufig Referenzübergabe genannt, obwohl die tatsächliche Argumentübergabe der Sprache weiterhin als Wert erfolgt.

Die Adresse im Aufruf und die Dereferenzierung im Funktionskörper haben unterschiedliche Aufgaben. &score teilt der Funktion mit, wo sich das Objekt befindet. *n = 0 gibt an, was dort gespeichert werden soll. Der Aufruf reset(score) liefert eine Ganzzahl, wo ein Zeiger erforderlich ist, und löst eine Compilermeldung aus.

Eine Vertauschung nachvollziehen
void swap(int *a, int *b) {
    int t = *a;
    *a = *b;
    *b = t;
}

/* Inside a calling function: */
int x = 3, y = 8;
swap(&x, &y);
  1. a zeigt auf x und b auf y.
  2. t speichert den ursprünglichen Wert von x, also 3.
  3. *a = *b schreibt 8 in x.
  4. *b = t schreibt die gespeicherte 3 in y.

Beide Variablen des Aufrufers ändern sich. Eine Version mit einfachen Parametern int a, int b vertauscht nur Kopien. Auch eine Zeigerversion mit int *t = a; a = b; b = t; vertauscht die Ganzzahlen nicht: Sie richtet nur die lokalen Zeigerparameter neu aus.

Mehr als ein Ergebnis liefern

Eine return-Anweisung liefert einen Wert, aber Zeigerparameter können zusätzliche Ausgaben übertragen:

void divide(int total, int size, int *q, int *r) {
    *q = total / size;
    *r = total % size;
}

/* size must be positive for these example inputs. */
int q, r;
divide(17, 5, &q, &r);

Die Ganzzahldivision ergibt für q den Wert 3 und für r den Wert 2. Der Aufrufer stellt zwei unterschiedliche beschreibbare Objekte bereit. Eine weitere nützliche Schnittstelle gibt einen Erfolgsstatus zurück und schreibt eine Ausgabe nur bei Erfolg; Aufrufer prüfen diesen Status, bevor sie die Ausgabe verwenden.

Dasselbe Muster in Bibliotheksaufrufen erkennen

scanf("%d", &age) benötigt die Adresse von age, weil die Funktion dort eine eingelesene Ganzzahl speichert. Die Übergabe von age liefert einen Wert statt des erforderlichen int * und führt zu undefiniertem Verhalten. Prüfe, ob scanf 1 zurückgibt, bevor du den neu gelesenen Wert verwendest.

Für Text übergibt char name[20]; scanf("%19s", name); einen Zeiger auf das erste Zeichen des Arrays. Die Breite erlaubt höchstens 19 Eingabezeichen und lässt Platz für '\0'. Der Arrayausdruck stellt den erforderlichen Zeichenzeiger bereits bereit.

Ein Arrayparameter erhält ebenfalls eine Adresse, keinen kopierten Block von Elementen. In void zero(int *a, size_t n) wirken sich Zuweisungen an a[i] auf das Array des Aufrufers aus. Bei der Übergabe der Adresse wird nur ein Zeiger kopiert. So können auch Funktionen, die nur lesen, große Arrays untersuchen, ohne jedes Element zu kopieren.

Wichtige Erkenntnisse

  • C kopiert jedes Argument, auch Adressen.
  • Ein Schreibzugriff über einen Zeigerparameter kann das Objekt des Aufrufers ändern.
  • Eine funktionierende Vertauschung von Ganzzahlen vertauscht *a und *b mithilfe eines gespeicherten Werts.
  • Für Ausgabeparameter muss der Aufrufer gültige beschreibbare Objekte bereitstellen.
  • scanf erhält Adressen und meldet, wie viele Konvertierungen erfolgreich waren.
  • Arrayparameter ermöglichen den gemeinsamen Zugriff auf Elemente des Aufrufers.

2.2 Const-Zugriff und sichere Rückgabewerte

Eine Zeigerschnittstelle sollte zwei Dinge klären: ob die Funktion darüber schreiben darf und wie lange der referenzierte Speicher gültig bleibt. const beantwortet die erste Frage. Die Lebensdauer des Objekts beantwortet die zweite.

Die const-Einschränkung zuordnen
DeklarationDarf p neu ausgerichtet werden?Darf über *p zugewiesen werden?
const int *p = &x;JaNein
int *const p = &x;NeinJa
const int *const p = &x;NeinNein

Ein Zeiger auf konstante Daten schränkt diesen Zugriffsweg ein. Wurde x als gewöhnliches int x = 5; definiert, ist x = 6; weiterhin erlaubt, auch während ein const int * darauf zeigt. Wurde das Objekt selbst mit const definiert, ist jeder Versuch, es über einen Zeiger zu ändern, undefiniertes Verhalten.

Ein konstanter Zeiger behält eine Adresse. Er muss initialisiert werden, und die Adresse kann später nicht ersetzt werden. Das Objekt an dieser Adresse kann sich weiterhin ändern, wenn sein Typ dies erlaubt. Lies vom Namen aus nach außen: int *const p macht p konstant; const int *p schützt den int, der über p erreicht wird.

Verwende einen nur lesenden Parameter wie const char *s, wenn eine Funktion eine Zeichenkette nur untersucht. Ein gewöhnlicher char * kann ohne explizite Typumwandlung an diesen Parameter übergeben werden. Die Funktion kann ihren lokalen Zeiger s weiterbewegen, aber eine Zuweisung über *s wird abgelehnt. Für die Rückumwandlung in einen beschreibbaren Typ reicht es nicht, eine Compilerwarnung zu beseitigen: Das zugrunde liegende Objekt muss tatsächlich veränderbar sein.

Eine Position im Speicher des Aufrufers zurückgeben
int *find(int *a, size_t n, int target) {
    for (size_t i = 0; i < n; i++) {
        if (a[i] == target) return &a[i];
    }
    return NULL;
}

Für int v[] = {4, 9, 2, 9}; gibt find(v, 4, 9) den Zeiger &v[1] zurück. Die Funktion endet, aber v gehört dem Aufrufer und existiert dort weiterhin. Nach der Prüfung des Ergebnisses auf NULL kann der Aufrufer found - v berechnen, um den Index 1 zu erhalten, oder mit *found = 0 das Element aktualisieren. Eine Suche nach 7 ergibt NULL, das nicht dereferenziert werden darf. strchr und strstr verwenden dieselbe Konvention eines Nullzeigers für „nicht gefunden“.

Adressen nicht mehr existierender lokaler Variablen zurückweisen
int *bad(void) {
    int result = 42;
    return &result;
}

result hat automatische Speicherdauer. Die Lebensdauer endet, wenn der Aufruf zurückkehrt, sodass der Aufrufer einen hängenden Zeiger erhält. Der Compiler kann warnen, und jeder spätere Zugriff über diesen Zeiger ist undefiniertes Verhalten. Dass die alten Bytes bei einem Lauf sichtbar sind, hält das Objekt nicht am Leben.

Dasselbe Problem kann an der schließenden Klammer eines inneren Blocks auftreten. Ein äußerer Zeiger kann die Lebensdauer einer inneren Variablen, deren Adresse er gespeichert hat, nicht verlängern.

Speicher mit ausreichender Lebensdauer wählen

Gib einen int-Wert zurück, wenn der Aufrufer nur ein ganzzahliges Ergebnis benötigt. Lass den Aufrufer für mehrere Ergebnisse Speicher zum Befüllen bereitstellen. Ein zurückgegebener Zeiger kann auch auf das Array des Aufrufers, ein über const char * zugängliches Zeichenkettenliteral oder ein static-Objekt verweisen.

Eine lokale statische Variable wird einmal initialisiert und existiert während des gesamten Programms, wird aber von allen Aufrufen gemeinsam genutzt. Speichert ein späterer Aufruf dort einen neuen Wert, sieht jeder zuvor zurückgegebene Zeiger diese Änderung. Statischer Speicher löst das Problem der Lebensdauer, ohne jedem Aufruf ein eigenes unabhängiges Ergebnisobjekt zu geben.

Wichtige Erkenntnisse

  • const int * schränkt Schreibzugriffe über diesen Zeiger ein, während int *const die Adresse festlegt.
  • Ein mit const definiertes Objekt darf nicht geändert werden.
  • Zeiger in das noch existierende Array des Aufrufers können nach der Rückkehr einer Funktion gültig bleiben.
  • Prüfe ein möglicherweise nullwertiges Suchergebnis vor dem Zugriff oder einer Subtraktion.
  • Die Rückgabe der Adresse einer lokalen Variablen mit automatischer Speicherdauer hinterlässt einen hängenden Zeiger.
  • Ein statisches Ergebnisobjekt wird von allen Aufrufen gemeinsam genutzt.
3

Arrays und Zeichenketten durchlaufen

Zeigerarithmetik verbindet die Adresse eines Elements mit seiner Position in einem Array. Der Typ des referenzierten Elements bestimmt die Schrittweite, das Array den zulässigen Bereich. Nutze dieses Modell, um Indexzugriffe zu lesen, einen Arrayausschnitt an eine Funktion zu übergeben und Schleifen zu erstellen, die vor einem ungültigen Zugriff stoppen. Bei Zeichenketten kommt eine zweite Abbruchregel hinzu: Ihr abschließendes Nullzeichen markiert das Ende des Textes, auch wenn der Speicher weitere Bytes enthält.

3.1 Elementschritte und Arraygrenzen

Die Addition einer Ganzzahl zu einem Zeiger bewegt ihn um Elemente des referenzierten Typs. Zeigt p auf das Arrayelement i, zeigt p + n auf Element i + n, sofern die entstehende Position innerhalb des Arrays oder unmittelbar hinter dessen Ende liegt.

Die Adresse einmal skalieren

Ausgehend von der Beispieladresse 0x1000 ergibt die Addition von 3 diese Ergebnisse:

ZeigertypGröße eines ElementsAdresse nach p + 3
int *4 Byte0x100C
double *8 Byte0x1018
char *1 Byte0x1003

Der Compiler führt die Skalierung durch. p + 3 * sizeof *p skaliert zweimal und führt im Allgemeinen zum falschen Element. Verwende unsigned char *, wenn die gewünschte Schrittweite tatsächlich ein Byte der Objektrepräsentation ist.

Für int a[6] = {10, 20, 30, 40, 50, 60}; und int *p = &a[1]; liest *(p + 2) das Element a[3], also 40. Die Zuweisung p += 3; versetzt den Zeiger selbst auf a[4] mit dem Wert 50. Von dort liest *(p - 1) den Wert 40. p++, p--, p += n und p -= n verwenden alle Elementschritte.

Positionen subtrahieren, um Elemente zu zählen

&a[5] - &a[1] ist 4, und die Umkehrung der Operanden ergibt -4. Das Ergebnis zählt bereits Elemente; dividiere es nicht durch sizeof(int). Sein vorzeichenbehafteter Typ ist ptrdiff_t, deklariert in <stddef.h>, und %td gibt es aus.

Subtrahiere nur Zeiger in dasselbe Array, einschließlich der Position unmittelbar hinter dessen Ende, und nur, wenn das Ergebnis in ptrdiff_t darstellbar ist. Diese Operation beschreibt Positionen innerhalb dieses Arrays. Die Subtraktion der Adressen nicht zusammengehöriger Variablen ist undefiniertes Verhalten, selbst wenn die ausgegebenen Zahlen zufällig nahe beieinanderliegen.

Die Position unmittelbar hinter dem Ende als Schranke verwenden

Bei sechs Elementen ist a + 6 ein gültiger Zeiger unmittelbar hinter das letzte Arrayelement. Er kann als Schranke dienen und bei gültigen Subtraktionen innerhalb desselben Arrays verwendet werden, darf aber nicht dereferenziert werden. a + 7 und a - 1 sind bereits beim Bilden undefiniertes Verhalten; eine spätere Dereferenzierung zu vermeiden, behebt das nicht.

for (int *p = a; p < a + 6; p++) {
    printf("%d\n", *p);
}

Der Schleifenkörper läuft für die Indizes 0 bis 5. Nach dem letzten Inkrement ist p gleich a + 6, und die nächste Prüfung stoppt die Schleife. Mit <= könnte der Schleifenkörper auf diese Position zugreifen, an der kein Element liegt.

Für die Regeln der Zeigerarithmetik verhält sich ein einzelnes Objekt wie ein Array mit einem Element. Du darfst &x + 1 bilden, aber nicht verwenden, um eine vermeintlich nächste Variable zu lesen.

Nur sinnvoll vergleichbare Positionen vergleichen

Ordnungsoperatoren wie < vergleichen Positionen innerhalb eines Arrays. Ordne keine Zeiger auf nicht zusammengehörige Objekte. Gleichheit und Ungleichheit lassen sich bei kompatiblen Zeigertypen vergleichen und zur Prüfung auf einen Nullzeiger verwenden.

Du kannst weder zwei Zeiger addieren noch Zeiger multiplizieren oder im Standard-C Arithmetik mit einem void * oder einem Funktionszeiger betreiben. Um die Mitte eines Bereichs [p, q) innerhalb desselben Arrays zu finden, verwende p + (q - p) / 2: Subtrahiere die Positionen, um eine Anzahl zu erhalten, dividiere diese Anzahl und gehe dann von p aus weiter.

Wichtige Erkenntnisse

  • Die Addition einer Ganzzahl versetzt einen Zeiger um Elemente seines Zieltyps.
  • Eine Zeigerdifferenz innerhalb desselben Arrays zählt bereits Elemente und hat den Typ ptrdiff_t.
  • Zeiger unmittelbar hinter das Ende dürfen eine Schranke markieren, aber nicht dereferenziert werden.
  • Einen Zeiger vor das Array oder über die Position unmittelbar hinter seinem Ende hinaus zu bilden, ist ungültig.
  • Verwende für Ordnungsvergleiche Positionen innerhalb desselben Arrays.
  • Subtrahiere Positionen, bevor du eine Mitte berechnest.

3.2 Arrayausdrücke und Zeigerparameter

Ein Array ist ein Block aus Elementen. Ein Zeiger ist ein separates Objekt, das eine Adresse enthält. Ihr enger Zusammenhang entsteht durch eine automatische Konvertierung, die in den meisten Ausdrücken stattfindet, nicht dadurch, dass die beiden Objekte derselben Art wären.

Einen Arrayausdruck konvertieren
int a[5] = {3, 1, 4, 1, 5};
int *p = a;

Hier wird a in einen Zeiger auf a[0] umgewandelt. Diese Array-zu-Zeiger-Konvertierung macht den Initialisierer gleichwertig zu &a[0]. Sowohl *p als auch a[0] lesen den Wert 3.

C definiert den Indexzugriff über Arithmetik: a[i] bedeutet *(a + i). Daher funktioniert auch p[3] und liest den Wert 1. &a[i] und a + i bezeichnen dasselbe Element. Sogar 3[a] hat dieselbe definierte Bedeutung wie a[3], obwohl die übliche Schreibweise viel klarer ist.

Erkennen, wann das gesamte Array ein Array bleibt

Die Konvertierung findet bei Operanden von sizeof oder dem unären & nicht statt. Sie findet auch nicht bei typeof und typeof_unqual aus C23 statt oder wenn ein Zeichenkettenliteral ein Zeichenarray initialisiert.

Ausdruck bei int mit 4 Byte und einem Zeiger mit 8 ByteErgebnis
sizeof a20 Byte für alle fünf Elemente
sizeof p8 Byte für das Zeigerobjekt
sizeof a / sizeof a[0]5 Elemente

Du kannst einem Array keine neue Adresse zuweisen und seinen Namen nicht erhöhen: a = p; und a++; sind ungültig. Du kannst p neu ausrichten, etwa mit p = a + 2;. Kopiere Arrayinhalte mit einer Schleife über die Elemente oder einem geeigneten memcpy-Aufruf.

Den angepassten Parametertyp verfolgen
int sum(const int *values, size_t n) {
    int total = 0;
    for (size_t i = 0; i < n; i++) total += values[i];
    return total;
}

Der Aufrufer stellt sowohl eine Adresse als auch eine Anzahl bereit. Für das obige Array besucht sum(a + 2, 3) die ursprünglichen Indizes 2, 3 und 4 und addiert 4, 1 und 5, um 10 zurückzugeben. Innerhalb der Funktion bezieht sich Index 0 auf den übergebenen Anfangszeiger.

In einer Funktionsparameterliste deklarieren int a[], int a[10] und int *a jeweils einen Zeigerparameter. Die einfache Schreibweise [10] liefert keine überprüfte Schranke und überträgt keine Länge. Innerhalb einer solchen Funktion misst sizeof a einen Zeiger. Der Ausdruck sizeof a / sizeof a[0] würde auf einem System mit 8-Byte-Zeigern und 4-Byte-int den Wert 2 ergeben, unabhängig von der tatsächlichen Arraylänge des Aufrufers.

Einen Elementzeiger von einem Arrayzeiger unterscheiden

a wird in int * umgewandelt, während &a den Typ int (*)[5] hat, einen Zeiger auf das gesamte Array mit fünf Elementen. Beide bezeichnen den Anfang desselben Speicherbereichs, aber die Arithmetik folgt ihren unterschiedlichen Typen. a + 1 rückt um einen int vor, bei dieser Anordnung also um 4 Byte. &a + 1 rückt um ein gesamtes Array oder 20 Byte bis zur Position unmittelbar hinter dessen Ende vor.

Eine ausgegebene Anfangsadresse kann diesen Typunterschied nicht erkennen lassen. Lies die Deklaration und notiere beim Verfolgen eines Ausdrucks den referenzierten Typ neben dem Zeiger.

Wichtige Erkenntnisse

  • Die meisten Arrayausdrücke werden in einen Zeiger auf das erste Element umgewandelt.
  • a[i] ist als *(a + i) definiert.
  • sizeof a misst das gesamte Array nur dort, wo a tatsächlich ein Array ist.
  • Ein Arrayparameter wird zu einem Zeiger angepasst; übergib seine nutzbare Länge separat.
  • &a zeigt auf ein ganzes Array, daher hat seine Arithmetik eine größere Schrittweite.

3.3 Zeigerschleifen über Zahlen und Text

Eine Zeigerschleife braucht eine aktuelle Position und eine Abbruchregel. Ein Array mit bekannter Elementanzahl verwendet einen Endzeiger; eine Zeichenkette verwendet ihr abschließendes '\0'. Wähle die Form, die am deutlichsten zeigt, welche Elemente besucht werden.

Einen Bereich mit bekannter Länge durchlaufen
int a[] = {3, 1, 4, 1, 5};
int total = 0;
for (const int *p = a; p < a + 5; p++) {
    total += *p;
}

Die laufenden Summen sind 3, 4, 8, 9 und 14. Eine indexbasierte Schleife, die a[i] für Indizes unter 5 addiert, liefert dasselbe Ergebnis. Moderne optimierende Compiler erzeugen für diese Formen häufig ähnlichen Code; die Zeigersyntax allein verspricht keinen Geschwindigkeitsvorteil.

Vor oder nach dem Versetzen lesen

Zeigt p in {3, 1, 4} auf die 3, setzt int v = *p++; die Variable v auf 3 und lässt p auf die 1 zeigen. Das Postfix-Inkrement liefert den alten Zeigerwert an * und aktualisiert den Zeiger. *++p geht zuerst zum nächsten Element und liest es dann. (*p)++ ändert das aktuelle Element und versetzt den Zeiger nicht.

Am abschließenden Nullzeichen stoppen
size_t my_strlen(const char *s) {
    const char *p = s;
    while (*p != '\0') p++;
    return (size_t)(p - s);
}

Bei "pointer" stoppt p auf dem abschließenden Nullzeichen, sieben Positionen vom Anfang entfernt. Das Ergebnis schließt dieses Nullzeichen nicht ein. Eine Zeichenzählung verwendet denselben Durchlauf: Beim Durchsuchen von "mississippi" und Erhöhen eines Zählers für jedes *p == 's' werden vier Treffer unter elf Buchstaben gezählt.

Ein Zeiger in die Mitte einer Zeichenkette ermöglicht den Zugriff auf ihren hinteren Teil. Bei const char *msg = "hello world"; gibt puts(msg + 6) den Text "world" aus. Die vorherigen Zeichen bleiben an ihrer Stelle.

Das abschließende Nullzeichen mitkopieren
char destination[4];
const char *src = "cat";
char *dst = destination;
while ((*dst++ = *src++) != '\0') {
}

Jede Iteration weist ein Zeichen zu, prüft den zugewiesenen Wert und setzt beide Zeiger weiter. Die letzte Iteration kopiert '\0', sodass das Ziel eine vollständige Zeichenkette ist. Beide Zeiger stehen danach unmittelbar hinter ihren abschließenden Nullzeichen. Der Speicher benötigt die Textlänge plus ein Byte; fehlt dieses zusätzliche Byte, entsteht ein Pufferüberlauf. Dieses Kopiermuster setzt außerdem voraus, dass Quell- und Zielbereich nicht überlappen.

Von beiden Enden aus umkehren

Ermittle bei einer beschreibbaren Zeichenkette zuerst ihre Länge. Ist die Länge null, stoppe, bevor du einen Zeiger auf das letzte Zeichen bildest. Setze andernfalls left = s und right = s + length - 1. Solange left < right gilt, vertausche die Zeichen, erhöhe left und verringere right. Für "stressed" ergeben vier Vertauschungen "desserts"; das abschließende Nullzeichen bleibt am ursprünglichen Ende.

Die Prüfung auf eine leere Zeichenkette ist wichtig, weil die Subtraktion von 1 von ihrem Anfangszeiger einen Zeiger vor das Array bilden würde.

Beschreibbaren Zeichenspeicher wählen

char word[] = "hello"; erstellt ein veränderbares Array mit sechs Byte einschließlich des abschließenden Nullzeichens. const char *word = "hello"; zeigt auf ein Zeichenkettenliteral mit statischer Speicherdauer. Ein Zeichenkettenliteral zu ändern, ist undefiniertes Verhalten, selbst wenn eine Deklaration char * ohne const verwendet. Verwende const char *, damit versehentliche Schreibzugriffe zu einem Übersetzungsfehler werden.

Eine Arrayschleife muss vor der Position unmittelbar hinter dem Ende stoppen, und ein Zeichenkettendurchlauf muss innerhalb des zugänglichen Speichers tatsächlich ein abschließendes Nullzeichen antreffen. Ein Zeiger enthält weder eine Länge noch eine Garantie, dass die Bytes eine Zeichenkette bilden.

Wichtige Erkenntnisse

  • Ein Bereich mit bekannter Länge endet bei einem Zeiger unmittelbar hinter dem Ende; eine Zeichenkette endet an ihrem Nullzeichen.
  • *p++ verwendet die alte Position und setzt den Zeiger weiter.
  • Das Kopieren einer Zeichenkette schließt das abschließende Nullzeichen ein und benötigt die Länge plus ein Byte.
  • Prüfe auf eine leere Zeichenkette, bevor du ihren Zeiger auf das letzte Zeichen bildest.
  • Mit Text initialisierte Zeichenarrays sind beschreibbar; Zeichenkettenliterale sind es nicht.
  • Ein Zeiger allein enthält weder eine Länge noch eine Garantie für eine Terminierung.
4

Einem weiteren Zeiger folgen und ihn aktualisieren

Ein Zeiger kann selbst das Objekt sein, das über eine Adresse erreicht wird. Das Verfolgen zweier Ebenen ermöglicht es einer Funktion, den Zeiger des Aufrufers zu ersetzen, und einem Programm, ein Array von Zeichenkettenadressen zu durchlaufen. Halte den Typ jeder Ebene fest: Ein Zeiger auf eine Ganzzahl, ein Zeiger auf diesen Zeiger und ein Zeiger auf eine ganze Zeile stehen für unterschiedliche Anordnungen. Verfolge, welches Objekt jede Dereferenzierung erreicht, bevor du bestimmst, was eine Zuweisung ändert.

4.1 Zwei Indirektionsebenen

Ein Zeiger ist ein gespeichertes Objekt und hat daher eine Adresse, die ein anderer Zeiger enthalten kann. Ein Zeiger auf einen Zeiger fügt einen weiteren Schritt zwischen dem Ausdruck und den endgültigen Daten hinzu.

Den Pfeilen einzeln folgen
int x = 7;
int *p = &x;
int **pp = &p;

Verwende Adressen zur Veranschaulichung: x bei 0x2000 enthält 7; p bei 0x3000 enthält 0x2000; pp bei 0x4000 enthält 0x3000. Die Auswertung von *pp erreicht p, dessen Wert den Typ int * hat. Die Auswertung von **pp folgt diesem Zeiger weiter zu x, einem int.

AusdruckTypErreichtes Objekt oder erreichter Wert
ppint **Adresse von p
*ppint *Der in p gespeicherte Zeiger
**ppintDie in x gespeicherte Ganzzahl

&p hat eine Zeigerebene mehr als p; jede Dereferenzierung entfernt eine Ebene. Die Initialisierung von int **pp mit &x passt nicht, weil &x den Typ int * hat.

Die Zuweisungsebene wählen

Bei einer weiteren noch existierenden Ganzzahl y ändert **pp = 9; die Variable x. Die Zuweisung *pp = &y; ändert den Zeiger p, sodass spätere Verwendungen von **pp die Variable y erreichen. Der aktuelle Wert von x bleibt unverändert. Ist q ein weiterer int *, ändert pp = &q; nur den äußeren Zeiger, sodass sein erster Pfeil statt p nun q erreicht.

Zähle nicht nur die Sternchen, sondern prüfe auch das Objekt auf jeder Ebene. Vor der Auswertung von **pp müssen sowohl der Weg zum Zeigerobjekt als auch der Weg von diesem Objekt zur Ganzzahl gültig sein.

Den Zeichenkettenzeiger eines Aufrufers versetzen
void skip_spaces(const char **s) {
    while (**s == ' ') {
        (*s)++;
    }
}

/* In the caller: */
const char *text = "   hi";
skip_spaces(&text);
puts(text);

Der Aufrufer übergibt die Adresse seiner Zeigervariablen. *s bezeichnet daher text; **s liest das aktuelle Zeichen. Jedes (*s)++ setzt text um ein Zeichen weiter. Nach drei Iterationen zeigt der Zeiger auf 'h', und puts gibt "hi" aus. Die Funktion benötigt einen gültigen Zeiger auf einen Zeiger in eine nullterminierte Zeichenkette.

Eine Version mit dem Parameter const char *s, die s erhöht, ändert nur eine lokale Kopie. Auch die Klammern in (*s)++ sind unverzichtbar: *s++ erhöht den äußeren lokalen Zeiger statt des Zeichenzeigers des Aufrufers. In const char **s sind die Zeichen über diesen Zugriffsweg schreibgeschützt, der Zeiger des Aufrufers darf aber versetzt werden.

Einen Ausgabezeiger einer Bibliotheksfunktion lesen

strtol aus <stdlib.h> verwendet einen Ausgabeparameter vom Typ char **:

char *end;
long number = strtol("42abc", &end, 10);

Die Funktion gibt 42 zurück und speichert in end einen Zeiger auf das erste nicht verwendete Zeichen, sodass der Zeiger nun "abc" erreicht. Ist der Endzeiger gleich dem Anfangszeiger, wurden keine Ziffern konvertiert. Eine vollständig geprüfte Zahlenkonvertierung behandelt auch Wertebereichsfehler; die Aufgabe des Zeigers ist hier, die Stoppposition anzugeben.

Wichtige Erkenntnisse

  • int **pp kann die Adresse eines int *-Objekts enthalten.
  • Jede Dereferenzierung folgt einer Zeigerebene.
  • Über **pp lässt sich die endgültige Ganzzahl ändern; über *pp lässt sich der dazwischenliegende Zeiger ändern.
  • Um den Zeiger eines Aufrufers zu aktualisieren, wird die Adresse dieses Zeigers benötigt.
  • strtol verwendet char **, um die Position des ersten nicht verwendeten Zeichens zurückzugeben.

4.2 Zeichenkettenzeigerarrays und rechteckige Arrays

Ein Array kann Zeiger als Elemente speichern. Ein zweidimensionales Array speichert dagegen ganze Zeilen als Elemente. Beide erlauben zwei Indizes, unterscheiden sich aber in ihrer Speicherung und den konvertierten Zeigertypen.

Adressen getrennter Zeichenketten speichern
const char *days[] = {"Mon", "Tue", "Wed"};

days enthält drei Zeichenzeiger. Jeder zeigt auf ein separates Zeichenkettenliteral. days[1] ist der Zeiger auf "Tue", und days[1][0] liest 'T'. Bei Objektzeigern mit 8 Byte ist sizeof days gleich 24, unabhängig von den Längen der Zeichenketten. Die Zeichenketten selbst belegen Speicher an anderer Stelle.

Die Längen können unterschiedlich sein, sodass eine unregelmäßige Sammlung entsteht. Um die Liste neu zu ordnen, können Zeigerelemente vertauscht werden, ohne den Text zu kopieren. Die Zuweisung days[0] = "Thursday"; ist erlaubt: Das Zeigerelement kann geändert werden, obwohl die darüber erreichten Zeichen schreibgeschützt sind.

In den meisten Ausdrücken wird days in einen Zeiger auf sein erstes Element umgewandelt. Da ein Element ein const char * ist, lautet der konvertierte Typ const char **:

for (const char **d = days; d < days + 3; d++) {
    puts(*d);
}

Die Schleife geht pro Iteration um ein Zeigerelement weiter. *d liefert den Zeichenzeiger, den puts benötigt.

Kommandozeilenzeichenketten lesen

int main(int argc, char *argv[]) erhält denselben Parametertyp wie char **argv. argc ist nicht negativ und zählt die verfügbaren Zeichenketten. Ist der Wert positiv, steht argv[0] für den Programmnamen oder für eine leere Zeichenkette, falls dieser Name nicht verfügbar ist. Benutzerargumente belegen die Indizes 1 bis argc - 1, und argv[argc] ist ein Nullzeiger.

Bei einem typischen Aufruf ./greet Ada 42 ist argc gleich 3, argv[1] zeigt auf "Ada" und argv[2] auf "42". Das letzte Argument ist Text, bis eine Konvertierung wie strtol es liest. argv[1][0] ist 'A'. Prüfe argc > 2, bevor du argv[2] verwendest.

Eine Schleife, die Benutzerargumente ausgibt, kann for (int i = 1; i < argc; i++) puts(argv[i]); verwenden. Um stattdessen von argv + 1 bis zu einem Nullzeiger zu gehen, prüfe zuerst argc > 0; ist argc null, gibt es keinen Bereich von Benutzerargumenten, der sich so beginnen lässt. Argumentzeichenketten sind im Gegensatz zu Zeichenkettenliteralen veränderbar.

Ganzen Zeilen folgen

int grid[3][4] enthält zwölf Ganzzahlen in zeilenweiser Speicherung: zuerst die gesamte Zeile 0, dann Zeile 1 und dann Zeile 2. Bei int mit 4 Byte belegt jede Zeile 16 Byte. grid[2][1] liegt 36 Byte hinter grid[0][0]: Zwei ganze Zeilen ergeben 32 Byte und die Spalte 4.

Deklaration oder AusdruckBedeutung
int (*rows)[4]Zeiger auf eine Zeile aus vier Ganzzahlen
int *ptrs[4]Array aus vier Ganzzahlzeigern
Konvertiertes gridint (*)[4]

grid + 1 rückt um eine Zeile vor. Eine Funktion, die es entgegennimmt, kann int g[][4] oder int (*g)[4] mit einer separaten Zeilenanzahl verwenden. Sie kann nicht int ** verwenden: Das Array enthält keine gespeicherten Zeilenzeiger, die sich dereferenzieren ließen. Eine explizite Typumwandlung kann diese fehlenden Zeiger nicht erzeugen.

Entsprechend besitzt char names[3][6] achtzehn Zeichenplätze in Zeilen fester Größe. const char *names[3] besitzt drei Zeigerplätze, die auf Zeichenketten unterschiedlicher Länge verweisen können.

Wichtige Erkenntnisse

  • Ein Array von Zeichenkettenzeigern speichert Adressen; der Text liegt anderswo.
  • Ein Zeigerarray wird in einen Zeiger auf sein erstes Zeigerelement umgewandelt.
  • argv[argc] ist ein Nullzeiger; prüfe die Argumentanzahl, bevor du eine bestimmte Zeichenkette liest.
  • Ein zweidimensionales Array speichert zusammenhängende Zeilen und enthält keine Zeigerobjekte für Zeilen.
  • int (*)[4] und int ** beschreiben unterschiedliche Speicheranordnungen.
  • Zwei Indizes allein belegen nicht, dass zwei Objekte denselben Typ haben.
5

Funktionen auswählen und Rückruffunktionen bereitstellen

Ein Funktionszeiger ermöglicht einem Programm, während der Ausführung eine Operation auszuwählen. Sein Typ legt fest, welcher Aufruf erlaubt ist, und ein Rückruffunktionsparameter ermöglicht einer anderen Funktion, diese Operation bei Bedarf aufzurufen. Verwende diese Ideen zuerst in einer kleinen Tabelle arithmetischer Funktionen und dann in qsort, wo die Rückruffunktion Elementadressen korrekt interpretieren und eine konsistente Reihenfolge zurückgeben muss. Beim Sortieren von Zeichenkettenzeigern kommen Funktionszeiger und zwei Ebenen von Objektzeigern zusammen.

5.1 Funktionszeiger deklarieren und über sie aufrufen

Ein Funktionszeiger speichert einen Verweis auf eine Funktion, die später aufgerufen werden kann. Sein Typ umfasst den Rückgabetyp und die Parametertypen. Diese Typen müssen zur Funktion passen, die über den Zeiger verwendet wird.

Den Zeigernamen in Klammern setzen
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }

int (*op)(int, int) = add;

Lies von op aus: Es ist ein Zeiger, das referenzierte Objekt ist eine Funktion, die zwei int entgegennimmt, und diese Funktion gibt einen int zurück. Ohne die Klammern deklariert int *op(int, int); eine Funktion, die einen Ganzzahlzeiger zurückgibt. Die Klammern ändern die Bedeutung der Deklaration.

In diesem Initialisierer wird ein Funktionsname in einen Funktionszeiger umgewandelt. op = &add; ist gleichwertig zu op = add;. Sowohl op(6, 3) als auch (*op)(6, 3) rufen add auf und geben 9 zurück. Die Zuweisung op = sub; führt dazu, dass derselbe Aufruf 3 zurückgibt. Am Aufrufausdruck hat sich nichts geändert, außer der im Zeiger gespeicherten Funktion.

Die Signatur kompatibel halten

Ein Zeiger vom Typ int (*)(int, int) kann eine Funktion, die double entgegennimmt und zurückgibt, nicht sicher aufrufen. Beispielsweise erzeugt die Zuweisung von sqrt an ihn eine Meldung über inkompatible Typen. Eine explizite Typumwandlung des Funktionszeigers, die eine Meldung unterdrückt, macht einen nicht passenden Aufruf nicht gültig; das Verhalten ist undefiniert.

Standard-C behandelt Objektzeiger und Funktionszeiger getrennt. void * ist der generische Objektzeigertyp und kein portabler Speicher für einen Funktionszeiger. Funktionszeiger unterstützen Prüfungen auf Gleichheit und Ungleichheit, aber keine Zeigerarithmetik.

Ein Null-Funktionszeiger bedeutet, dass keine aufrufbare Funktion vorhanden ist. Erlaubt eine Schnittstelle diesen Zustand, prüfe ihn:

if (op != NULL) {
    int result = op(6, 3);
    printf("%d\n", result);
}

Ein Aufruf über einen Null-Funktionszeiger ist undefiniertes Verhalten.

Den Typ einmal benennen
typedef int (*binop)(int, int);
binop ops[] = {add, sub, mul};

Das typedef benennt einen Zeigertyp. binop selected = mul; deklariert einen Zeiger; binop pick(char symbol); deklariert eine Funktion, die einen solchen zurückgibt. Ohne das typedef würde diese Rückgabedeklaration int (*pick(char symbol))(int, int); lauten.

Ein Array von Funktionszeigern ist eine Sprungtabelle. ops[2](6, 3) wählt mul aus und gibt 18 zurück. Prüfe bei einem vorzeichenbehafteten Auswahlindex i die Bedingung i >= 0 && i < 3, bevor du ops[i] auswertest. Ein Indexzugriff außerhalb der Tabelle ist ungültig, selbst wenn jede gespeicherte Funktion ansonsten aufrufbar ist.

Größere Deklarationen von innen nach außen lesen

Beginne beim Namen und löse Klammern und Postfixteile auf, bevor du weiter nach außen gehst. In int (*table[3])(int, int) macht table[3] ein Array aus drei Elementen, * macht jedes Element zu einem Zeiger, und (int, int) macht dessen Ziel zu einer Funktion, die int zurückgibt.

In char *(*f)(const char *) ist f ein Zeiger auf eine Funktion, die einen const char * entgegennimmt; diese Funktion gibt einen char * zurück. Halte die Anforderungen an die Lebensdauer des zurückgegebenen Zeigers von den Anforderungen an kompatible Aufrufe über den Funktionszeiger getrennt.

Wichtige Erkenntnisse

  • Klammern unterscheiden einen Funktionszeiger von einer Funktion, die einen Zeiger zurückgibt.
  • Ein Funktionszeiger hält die Rückgabe- und Parametertypen gültiger Aufrufe fest.
  • Ein Funktionsname kann einen kompatiblen Funktionszeiger ohne & initialisieren.
  • Ein typedef kann Tabellen und Rückgabetypen von Funktionszeigern leichter lesbar machen.
  • Prüfe den Tabellenindex und einen gegebenenfalls vorhandenen Nullzeiger vor dem Aufruf.

5.2 Rückruffunktionen und korrekte qsort-Vergleiche

Eine Rückruffunktion ist eine Funktion, die einer anderen Funktion übergeben wird, welche sie im Rahmen ihrer Arbeit aufruft. Das trennt die allgemeine Durchlauf- oder Sortieroperation von der konkreten Aktion oder Reihenfolge, die du auswählst.

Eine Operation übergeben, ohne sie aufzurufen
void twice(int *x) { *x *= 2; }

void for_each(int *a, size_t n, void (*fn)(int *)) {
    for (size_t i = 0; i < n; i++) fn(&a[i]);
}

Für int v[] = {1, 2, 3}; erzeugt for_each(v, 3, twice); die Werte {2, 4, 6}. Der Funktionsname liefert einen Zeiger. twice() würde versuchen, die Funktion sofort ohne das erforderliche Argument aufzurufen, statt eine Operation für spätere Aufrufe bereitzustellen.

Den Typ aus einem generischen Objektzeiger zurückgewinnen

Ein void * kann einen Objektzeiger aufnehmen und zurück in den ursprünglichen Typ umgewandelt werden. Er kann nicht dereferenziert oder in Standard-C für Zeigerarithmetik verwendet werden, weil er keinen Elementtyp vorgibt. Erhalte Qualifizierer: Schreibgeschützte Daten werden über const void * und anschließend über einen geeigneten Zeiger auf konstante Daten weitergegeben.

qsort aus <stdlib.h> erhält eine Basisadresse, eine Elementanzahl, eine Elementgröße in Bytes und eine Vergleichsfunktion als Rückruffunktion. Die Rückruffunktion hat den Typ int (*)(const void *, const void *). Jedes Argument zeigt auf ein Arrayelement, und das Vorzeichen des Rückgabewerts gibt an, wohin das erste Element gehört:

Vorzeichen des RückgabewertsReihenfolge
NegativErstes vor zweitem
NullFür diese Reihenfolge gleichwertig
PositivErstes nach zweitem
Ganzzahlen ohne Überlauf sortieren
int cmp_int(const void *a, const void *b) {
    int x = *(const int *)a;
    int y = *(const int *)b;
    return (x > y) - (x < y);
}

/* In the caller: */
int v[] = {42, -7, 19, 0, 3};
qsort(v, 5, sizeof v[0], cmp_int);

Das Array wird zu {-7, 0, 3, 19, 42}. Vergleiche ergeben 0 oder 1, sodass der Rückgabeausdruck nur -1, 0 oder 1 ergibt. Kehrst du den Ordnungs­ausdruck zu (y > x) - (y < x) um, wird absteigend sortiert.

Verwende return x - y; nicht für beliebige vorzeichenbehaftete Ganzzahlen. Bei einem int mit 32 Bit überschreitet die Subtraktion von -2000000000 von 2000000000 den Wert INT_MAX. Ein Überlauf bei vorzeichenbehafteten Ganzzahlen ist undefiniertes Verhalten, sodass weder ein Ergebnis durch Wraparound noch ein zuverlässiges Vergleichsvorzeichen zugesichert ist.

Zeiger auf Zeichenketten sortieren
int cmp_str(const void *a, const void *b) {
    return strcmp(*(const char *const *)a,
                  *(const char *const *)b);
}

const char *names[] = {"Lin", "Ada", "Grace"};
qsort(names, 3, sizeof names[0], cmp_str);

Binde für strcmp die Headerdatei <string.h> ein. Ein Arrayelement ist hier selbst ein Zeiger. Die Rückruffunktion erhält eine Adresse dieses Zeigerelements, sodass einmaliges Dereferenzieren die Zeichenkettenadresse liefert. Das hinzugefügte innere const verhindert, dass das Element über diesen Zugriffsweg geändert wird. Nach dem Sortieren ist die Reihenfolge "Ada", "Grace", "Lin".

strcmp(a, b) würde die Bytes der Zeigerplätze als Text lesen. Verwende außerdem sizeof names[0], die Größe eines Zeigerelements, nicht sizeof(char) oder die Länge eines Namens.

Vergleichsfunktionen dürfen Elemente nicht ändern und müssen konsistente Ergebnisse liefern, einschließlich einer transitiven Reihenfolge. qsort verspricht nicht, die Reihenfolge von Elementen zu erhalten, die als gleich verglichen werden. bsearch akzeptiert dieselbe Form von Vergleichsfunktion, um ein bereits nach dieser Reihenfolge sortiertes Array zu durchsuchen.

Wichtige Erkenntnisse

  • Übergib einen Funktionsnamen, um eine Rückruffunktion bereitzustellen; Klammern rufen eine Funktion sofort auf.
  • Gewinne vor dem Zugriff aus jedem generischen Vergleichsargument den Elementtyp zurück.
  • qsort benötigt eine Anzahl, die Größe eines Elements und eine kompatible Vergleichsfunktion.
  • Verwende Vergleichsvorzeichen ohne eine möglicherweise überlaufende Subtraktion.
  • Vergleichsfunktionen für Zeichenkettenzeiger erhalten Adressen von Zeigerplätzen.
  • Gleiche Vergleichsergebnisse garantieren nach qsort keine stabile Reihenfolge.

Zeiger und Speicheradressen in C: Beispiele

5 Interaktive Lektionen

1

Eine Adresse und den darüber erreichten Wert verfolgen

13 Schritte
Erstelle ein Speicherbild, richte Zeiger neu aus und sage voraus, welches Objekt jede Zuweisung ändert.
  1. 1
    EinführungDrei Dinge auseinanderhalten

    Eine gespeicherte Ganzzahl hat einen Wert, einen Ort und eine Lebensdauer. Ein Zeiger fügt ein weiteres Objekt hinzu, dessen Wert eine Adresse ist. Verfolge ein kleines Programm, indem du beide Arten von Werten festhältst: Ganzzahlen wie 10 und Adressen wie 0x1000.

    Die hilfreiche Frage in jeder Zeile ist konkret: Ändert diese Anweisung eine Adresse, ändert sie das über diese Adresse erreichte Objekt oder liest sie nur einen Wert?

  2. 2
    ErklärungVier Ganzzahlen anordnen

    Stell dir vor, int scores[4] beginnt auf einem System mit Ganzzahlen von 4 Byte bei 0x1000. Das erste Element belegt die Bytes 0x1000 bis 0x1003. Das nächste beginnt bei 0x1004, gefolgt von 0x1008 und 0x100C.

    Die Elemente liegen aufeinanderfolgend im Speicher. Diese Anordnungsregel gilt für das Array; sie garantiert keine Reihenfolge für zwei getrennt deklarierte Ganzzahlen.

  3. 3
    BeispielDas Array und dann seinen Inhalt messen

    Für char word[20] = "hi"; ergeben zwei Messungen unterschiedliche Ergebnisse:

    • sizeof word ergibt 20 Byte Arrayspeicher.
    • strlen(word) ergibt 2 Zeichen vor '\0'.

    Das abschließende Nullzeichen belegt ein weiteres Arrayelement, und die übrigen Elemente gehören weiterhin zum Array. sizeof gibt einen Wert vom Typ size_t zurück, der mit %zu ausgegeben wird; es zählt nicht, wie viele Elemente das Programm derzeit verwendet.

  4. 4
    VerbindungAnordnung und Lebensdauer unterscheiden

    Die Speicherausrichtung bestimmt geeignete Anfangsadressen, während die Byte-Reihenfolge die Reihenfolge der Bytes innerhalb eines Werts bestimmt. Keine der beiden sagt dir, wie lange ein Objekt existiert.

    Eine gewöhnliche lokale Variable existiert während der Ausführung ihres Blocks. Eine lokale statische oder globale Variable existiert während des gesamten Programms. Das Speichern einer Adresse hält einen Weg zum Speicher fest; es kann eine lokale Variable mit automatischer Speicherdauer nach dem Ende des Blocks nicht am Leben halten.

  5. 5
    HerausforderungDie Adresse eines Elements finden

    Ein Ganzzahlarray mit sechs Elementen beginnt bei 0x1800, mit 4 Byte pro Element. Wo beginnt Element 3, und wie groß ist das gesamte Array?

    ---reveal---

    Element 3 beginnt zwölf Byte nach dem Anfang, bei 0x180C. Sechs Elemente mit je 4 Byte belegen 24 Byte. Der Index gibt einen Versatz in Elementen an; die Multiplikation mit der Elementgröße ergibt den Versatz in Bytes.

  6. 6
    ErklärungDem Zeiger ein Ziel geben

    Betrachte int count = 42; int *p = &count;. Der Operator & ermittelt die Adresse von count, und p speichert diese Adresse. Liegt count an der Beispieladresse 0x2000, ist der Wert des Zeigers 0x2000; der Wert der Ganzzahl bleibt 42.

    Der Zeiger belegt auch eigenen Speicher. Seine Adresse ist &p; das ist eine andere Information als die in p enthaltene Adresse.

  7. 7
    TippDas Sternchen neben jedem Namen lesen

    int *p, q; deklariert einen Zeiger und eine gewöhnliche Ganzzahl. Um zwei Zeiger zu deklarieren, verwende int *p, *q; oder schreibe die Deklarationen in getrennte Zeilen.

    Initialisiere einen Zeiger mit einer geeigneten Objektadresse oder NULL. Ein Nullzeiger ist ein bekannter leerer Zustand; ein nicht initialisierter lokaler Zeiger hat einen unbestimmten Wert und kann nicht sicher so geprüft werden, als wäre er bereits ein Nullzeiger.

  8. 8
    HerausforderungNeu ausrichten, ohne Daten zu ändern

    Beginne mit int a = 9, b = 16; int *p = &a;. Führe dann p = &b; aus. Welche Ganzzahlwerte ändern sich und was enthält p nun?

    ---reveal---

    Keine der beiden Ganzzahlen ändert sich: a bleibt 9 und b bleibt 16. Nur p ändert sich und speichert die Adresse von b. Eine Zeigerzuweisung ersetzt eine Adresse; sie kopiert weder den Inhalt des alten noch den des neuen Ziels.

  9. 9
    BeispielÜber p lesen, addieren und schreiben

    Fahre mit int x = 10; int *p = &x; fort und führe aus:

    *p = *p + 5;
    

    Das *p auf der rechten Seite liest 10 aus x. Die Addition von 5 ergibt 15. Das *p auf der linken Seite bezeichnet dasselbe Objekt, sodass die Zuweisung 15 in x schreibt. Die Adresse in p hat sich nicht geändert. Anschließend liest int y = *p * 2; den Wert 15 und erstellt eine separate Ganzzahl y, die 30 enthält.

  10. 10
    KernaussageEine Adresskopie schafft einen weiteren Weg

    Bei int n = 6; int *p = &n; int *q = p; erreichen beide Zeiger die Variable n. Die Zuweisung hat eine Adresse kopiert, nicht das Ganzzahlobjekt.

    Nach *q = 14; ergibt das Lesen von n, *p oder *q den Wert 14. Zeichne ein Ganzzahlkästchen und zwei Pfeile dorthin. Zwei Ganzzahlkästchen würden fälschlich unabhängige Daten nahelegen.

  11. 11
    TippDas Ziel vor dem Zugriff prüfen

    Ein Zugriff benötigt einen Zeiger auf ein noch existierendes Objekt eines geeigneten Typs. Ein Nullzeiger bezeichnet kein Objekt, und ein hängender Zeiger hat sein noch existierendes Ziel verloren. Eine Nullprüfung behandelt den optionalen leeren Zustand eines initialisierten Zeigers; sie belegt nicht, dass ein Ziel eines Nicht-Nullzeigers noch existiert.

    Ein ungültiger Zugriff ist undefiniertes Verhalten. C verspricht weder einen bestimmten Wert noch einen Absturz, selbst wenn ein Programmlauf normal erscheint.

  12. 12
    HerausforderungEiner kopierten Adresse folgen

    Was sind die endgültigen Werte von a und b?

    int a = 6, b = 19;
    int *p = &a, *q = p;
    p = &b;
    *q = *p + 1;
    

    ---reveal---

    a wird zu 20 und b bleibt 19. Das Neuausrichten von p lässt q weiterhin auf a zeigen. Die letzte Zeile liest b über p und schreibt die Summe über q nach a.

  13. 13
    ZusammenfassungDas auf der linken Seite bezeichnete Objekt verfolgen

    Lies die linke Seite, bevor du eine Änderung vorhersagst. Eine Zuweisung an p ändert eine Adresse; eine Zuweisung an *p ändert dessen Ziel. Stelle kopierte Zeiger als separate Pfeile dar, prüfe die Lebensdauer des Ziels und verwende den referenzierten Typ, um jeden Zugriff zu interpretieren.

2

Eine Funktion Daten des Aufrufers ändern lassen

11 Schritte
Verfolge eine Vertauschung, verwende Ausgabeparameter und entscheide, ob zurückgegebene Zeiger gültig bleiben.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungDer Adresse in einen Aufruf folgen
  2. 2
    ErklärungAuch eine kopierte Adresse erreicht den Aufrufer
  3. 3
    BeispielDen Wert vor dem Ersetzen speichern
  4. 4
    BeispielDen zweiten Wert an die erste Stelle schreiben
  5. 5
    BeispielDie Vertauschung abschließen
  6. 6
    HerausforderungZwei Ausgaben liefern
  7. 7
    Erklärungconst auf die vorgesehene Ebene setzen
  8. 8
    BeispielEine Position in einem noch existierenden Objekt zurückgeben
  9. 9
    HerausforderungDas Problem mit der Lebensdauer erklären
  10. 10
    HerausforderungEine beschreibbare Suchschnittstelle wählen
  11. 11
    ZusammenfassungSchreibzugriff und Lebensdauer prüfen
3

Ein Array durchlaufen, ohne seine Grenze zu überschreiten

15 Schritte
Verbinde Arithmetik, Arraykonvertierung und Zeichenkettenschleifen, indem du jede besuchte Position verfolgst.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungAnfang, Schritt und Ende finden
  2. 2
    ErklärungUm Elemente weitergehen
  3. 3
    BeispielZwei Positionen subtrahieren
  4. 4
    KernaussageEine Schranke verwenden, die kein Element ist
  5. 5
    HerausforderungDie Mitte bestimmen
  6. 6
    ErklärungEin Arrayausdruck liefert einen Anfangszeiger
  7. 7
    Beispielsizeof an der richtigen Stelle anwenden
  8. 8
    HerausforderungEinen kleineren Bereich übergeben
  9. 9
    BeispielErst lesen und dann weitergehen oder umgekehrt
  10. 10
    ErklärungDas Ende einer Zeichenkette finden
  11. 11
    BeispielZeichen für Zeichen kopieren
  12. 12
    TippDen Platz für das abschließende Nullzeichen reservieren
  13. 13
    HerausforderungDen letzten Schleifenbesuch korrigieren
  14. 14
    HerausforderungEine sichere Vorbereitung für das Umkehren wählen
  15. 15
    ZusammenfassungJede besuchte Position ausdrücklich bestimmen
4

Einen Zeiger ändern und ein Zeigerarray lesen

12 Schritte
Verfolge zwei Indirektionsebenen und unterscheide dann Zeichenkettenzeigerplätze von vollständigen Arrayzeilen.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungDen Zeiger selbst zum Ziel machen
  2. 2
    BeispielZwei Pfeile zeichnen
  3. 3
    ErklärungWählen, welche Ebene sich ändert
  4. 4
    BeispielDie Position des Aufrufers über eine Adresse übergeben
  5. 5
    HerausforderungEine Änderung nur am Zeiger vorhersagen
  6. 6
    VerbindungDie Stoppposition einer Konvertierung lesen
  7. 7
    ErklärungEin Array kann Zeigerplätze besitzen
  8. 8
    BeispielDurch Adressen weitergehen
  9. 9
    TippArgumentanzahl vor dem Zeichenkettenzugriff prüfen
  10. 10
    HerausforderungEinen Typ für eine ganze Zeile lesen
  11. 11
    HerausforderungEine Funktion an den bereitgestellten Speicher anpassen
  12. 12
    ZusammenfassungDas Objekt auf jeder Ebene benennen
5

Eine Operation auswählen und über eine Rückruffunktion sortieren

15 Schritte
Lies Funktionszeigertypen, wähle eine Operation aus einer Tabelle und erstelle Vergleichsfunktionen für Ganzzahlen und Zeichenkettenzeiger.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungEine allgemeine Aufgabe von ihrer ausgewählten Operation trennen
  2. 2
    ErklärungDen aufrufbaren Typ lesen
  3. 3
    BeispielEine andere Funktion auswählen
  4. 4
    TippEinen Zeigertyp für eine Tabelle benennen
  5. 5
    HerausforderungEine verwandte Deklaration lesen
  6. 6
    ErklärungEine Aktion als Rückruffunktion bereitstellen
  7. 7
    BeispielDie vier Sortierargumente bereitstellen
  8. 8
    ErklärungDen Ganzzahlelementtyp zurückgewinnen
  9. 9
    BeispielDas Vorzeichen der Reihenfolge zurückgeben
  10. 10
    HerausforderungEine überlaufende Abkürzung vermeiden
  11. 11
    VerbindungZeichenkettenzeiger statt Zeichenzeilen sortieren
  12. 12
    HerausforderungDie richtige Sortiergröße wählen
  13. 13
    KernaussageGleichheit verspricht keine Stabilität
  14. 14
    HerausforderungEine ausgewählte Vergleichsfunktion verfolgen
  15. 15
    ZusammenfassungAufruf, Anordnung und Reihenfolge aufeinander abstimmen

Zeiger und Speicheradressen in C: Übungsaufgaben

40 Übungsfragen in 4 Sets

Set 1: Adressen und Funktionsaufrufe verfolgen10 Übungsfragen

Frage 1 von 10

Ein int belegt 4 Byte. Ein Array int levels[7] beginnt an der Beispieladresse 0x2400. An welcher Adresse beginnt levels[4]?
  1. 0x2404
  2. 0x2410
  3. 0x2414
  4. 0x241C
Antwort zeigenAntwort ausblenden
Antwort: B
Index 4 liegt vier Elemente hinter dem Anfang. Jedes belegt 4 Byte, sodass der Versatz 16 Byte beträgt, geschrieben 0x10, und die Adresse 0x2410 ist. Würde man nur 4 zur Adresse addieren, würde der Index als Byteanzahl behandelt und stattdessen das zweite Element erreicht.

Frage 2 von 10

Welche Deklaration erstellt bei int items = 16; einen Zeiger auf items?
  1. int *p = items;
  2. int p = &items;
  3. int *p = *items;
  4. int *p = &items;
Antwort zeigenAntwort ausblenden
Antwort: D
&items ermittelt die Adresse der Ganzzahl, und int *p deklariert den passenden Zeigertyp. Die Initialisierung des Zeigers mit items würde die gespeicherte Ganzzahl 16 statt der Adresse eines Ganzzahlobjekts liefern. Das Ermitteln einer Adresse lässt den Wert von items unverändert.

Frage 3 von 10

Welchen Wert enthält n nach diesem Code?

int n = 14;
int *p = &n;
*p = 23;
  1. 23
  2. 14
  3. 37
  4. 0
Antwort zeigenAntwort ausblenden
Antwort: A
p enthält die Adresse von n, sodass *p dieselbe Ganzzahl bezeichnet. Die Zuweisung von 23 über *p ersetzt den Wert in n. Wer 14 erwartet, verwechselt einen Schreibzugriff über den Zeiger mit einer Änderung der im Zeiger selbst gespeicherten Adresse.

Frage 4 von 10

Welchen Wert hat score, nachdem clear(score) zurückkehrt?

void clear(int value) { value = 0; }
/* In the caller: */
int score = 27;
clear(score);
  1. 0
  2. 1
  3. 27
  4. Undefiniertes Verhalten
Antwort zeigenAntwort ausblenden
Antwort: C
Die Funktion erhält eine lokale Kopie des Ganzzahlarguments. Das Setzen von value auf null ändert diesen Parameter, sodass score beim Aufrufer 27 bleibt. Für den erwarteten Wert null wäre eine Funktion nötig, die &score über einen Ganzzahlzeiger erhält und in das referenzierte Objekt schreibt.

Frage 5 von 10

Was gibt dies auf einem System mit Objektzeigern von 8 Byte aus?

char label[12] = "oak";
char *p = label;
printf("%zu %zu
", sizeof label, sizeof p);
  1. 4 8
  2. 12 8
  3. 12 12
  4. 3 8
Antwort zeigenAntwort ausblenden
Antwort: B
label ist ein Array aus zwölf Zeichen, daher ist sizeof label gleich 12, obwohl der gespeicherte Text nur drei Buchstaben hat. p ist ein separates Zeigerobjekt und belegt auf diesem System 8 Byte. Das Mitzählen des abschließenden Nullzeichens ergibt den belegten Anfang der Zeichenkette, nicht die deklarierte Arraygröße.

Frage 6 von 10

Welche Werte haben left und right nach dem Aufruf?

void exchange(int *a, int *b) {
    int *t = a;
    a = b;
    b = t;
}
/* In the caller: */
int left = 11, right = 29;
exchange(&left, &right);
  1. 29, 11
  2. 29, 29
  3. 11, 11
  4. 11, 29
Antwort zeigenAntwort ausblenden
Antwort: D
Die Funktion vertauscht ihre lokalen Zeigerkopien, ohne *a oder *b einen Wert zuzuweisen. Es wird kein Ganzzahlobjekt beschrieben, sodass left 11 und right 29 bleibt. Wer erwartet, dass die Werte vertauscht werden, verwechselt das Versetzen lokaler Adresswerte mit dem Kopieren der über diese Adressen erreichten Ganzzahlen.

Frage 7 von 10

Eine Funktion ist als int read_count(int *out); deklariert. Sie gibt bei Erfolg 1 zurück und schreibt die Anzahl nur bei Erfolg. Welcher Code des Aufrufers gibt eine neu gelesene Anzahl sicher aus?
  1. int n; if (read_count(&n)) printf("%d", n);
  2. int n; read_count(&n); printf("%d", n);
  3. int n; if (read_count(n)) printf("%d", n);
  4. int *n; if (read_count(n)) printf("%d", *n);
Antwort zeigenAntwort ausblenden
Antwort: A
Der Aufrufer stellt ein Ganzzahlobjekt über dessen Adresse bereit und verwendet seinen Wert erst, nachdem der Erfolgsstatus wahr ist. Eine bedingungslose Ausgabe könnte ein nicht initialisiertes n lesen, wenn die Funktion fehlschlägt, ohne zu schreiben. Ein nicht initialisierter Zeiger stellt kein gültiges Ausgabeobjekt bereit, das die Funktion befüllen kann.

Frage 8 von 10

Welche Zuweisung ist bei int x = 6, y = 17; const int *p = &x; erlaubt?
  1. *p = y;
  2. (*p)++;
  3. p = &y;
  4. *p += 1;
Antwort zeigenAntwort ausblenden
Antwort: C
p ist ein Zeiger auf konstante Ganzzahldaten und darf daher auf y neu ausgerichtet werden. Eine Zuweisung über *p wird abgelehnt, weil dieser Zugriffsweg schreibgeschützt ist. Die Einschränkung gilt für den Zugriff über diesen Zeiger; x selbst wurde ohne const definiert und kann weiterhin über seinen eigenen Namen geändert werden.

Frage 9 von 10

Was sind die endgültigen Werte von a und b?

int a = 4, b = 13;
int *p = &a, *q = p;
*p += 3;
p = &b;
*q = *p - 2;
*p = *q + 5;
  1. 7, 12
  2. 11, 13
  3. 7, 18
  4. 11, 16
Antwort zeigenAntwort ausblenden
Antwort: D
Der erste Schreibzugriff setzt a auf 7. Nach dem Neuausrichten von p zeigt q weiterhin auf a. Die nächste Zuweisung setzt a mithilfe des Werts 13 von b auf 11; der letzte Schreibzugriff setzt dann b auf 16. Wer für a den Wert 7 annimmt, versetzt fälschlich auch q, obwohl nur p neu ausgerichtet wurde.

Frage 10 von 10

Welches Zahlenpaar gibt die letzte Zeile aus?

int *next_value(void) {
    static int n = 20;
    n += 3;
    return &n;
}
/* In the caller: */
int *first = next_value();
int saved = *first;
int *second = next_value();
printf("%d %d
", saved, *first + *second);
  1. 23 49
  2. 23 52
  3. 26 52
  4. 23 46
Antwort zeigenAntwort ausblenden
Antwort: B
Der erste Aufruf speichert 23; dieser Wert wird nach saved kopiert. Der zweite Aufruf ändert dieselbe statische Ganzzahl zu 26, und beide zurückgegebenen Zeiger erreichen sie, sodass sich ihre Werte zu 52 addieren. Das Ergebnis 49 behandelt den ersten Zeiger wie eine erhaltene Momentaufnahme, aber nur saved enthält eine separat kopierte Ganzzahl.
Set 2: Arraybereiche und Zeichenketten untersuchen10 Übungsfragen

Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.

Frage 1 von 10

Was ist &values[6] - &values[2] für double values[8];?
  1. 32
  2. 8
  3. 4
  4. 2
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 2 von 10

Was gibt puts(message + 5) für const char *message = "blue sky"; aus?
  1. sky
  2. sky
  3. blue
  4. ky
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 3 von 10

Welchen Wert hat *(p - 2) nach diesen Anweisungen?

int bins[] = {6, 12, 18, 24, 30, 36, 42};
int *p = bins + 1;
p += 4;
  1. 18
  2. 36
  3. 12
  4. 24
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 4 von 10

Was gibt total(data + 1, 3) zurück?

int total(const int *p, size_t n) {
    int s = 0;
    for (size_t i = 0; i < n; i++) s += p[i];
    return s;
}
/* In the caller: */
int data[] = {9, 4, 7, 2, 8};
  1. 20
  2. 13
  3. 17
  4. 21
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 5 von 10

Was gibt count(values) auf einem System mit Zeigern von 8 Byte und int von 4 Byte zurück?

size_t count(int a[9]) {
    return sizeof a / sizeof a[0];
}
/* In the caller: */
int values[9] = {0};
  1. 2
  2. 9
  3. 8
  4. 4
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 6 von 10

Welche Werte haben taken und a[0] danach?

int a[] = {8, 15, 22};
int *p = a;
int taken = *p++;
(*p)++;
  1. 15, 9
  2. 8, 9
  3. 8, 8
  4. 9, 8
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 7 von 10

Was ist nach diesem Code die größte nicht negative Ganzzahl k, für die p + k innerhalb des zulässigen Zeigerbereichs gebildet werden kann?

int a[8];
int *p = a + 1, *end = a + 8;
p += (end - p) / 2;
end -= 2;
p += end - p;
  1. 3
  2. 2
  3. 1
  4. 4
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 8 von 10

Eine beschreibbare nullterminierte Zeichenkette wird mit diesem Aufbau umgekehrt. Welche Eingabe zeigt eine ungültige Zeigerberechnung, bevor die Schleife beginnt?

size_t n = strlen(s);
char *left = s;
char *right = s + n - 1;
while (left < right) {
    char t = *left; *left++ = *right; *right-- = t;
}
  1. Eine Zeichenkette mit zwei Zeichen
  2. Eine Zeichenkette mit einem Zeichen
  3. Eine Zeichenkette mit ungerader Länge
  4. Eine leere Zeichenkette
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 9 von 10

Ein Array int a[6] beginnt an der Beispieladresse 0x3000, und jede Ganzzahl belegt 4 Byte. Welche Adressen stellen a + 1 und &a + 1 dar?
  1. 0x3004 und 0x3018
  2. 0x3004 und 0x3004
  3. 0x3018 und 0x3018
  4. 0x3001 und 0x3006
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 10 von 10

Welche Werte haben count und p - text nach dieser Schleife?

char text[] = "abaca";
char *p = text;
int count = 0;
while (*p != '\0') {
    if (*p++ == 'a') count++;
}
  1. 3, 6
  2. 2, 5
  3. 3, 5
  4. 2, 6
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Set 3: Zeigerebenen und Speicheranordnungen verfolgen10 Übungsfragen

Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.

Frage 1 von 10

Welchen Typ hat *pp nach int n = 31; int *p = &n; int **pp = &p;?
  1. int
  2. int **
  3. int ***
  4. int *
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 2 von 10

Was ist sizeof words bei Objektzeigern mit 8 Byte?

const char *words[] = {"red", "violet", "gold", "teal"};
  1. 20
  2. 32
  3. 24
  4. 8
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 3 von 10

Was ist *p danach?

int a = 18, b = 25;
int *p = &a;
int **pp = &p;
*pp = &b;
**pp -= 4;
  1. 21
  2. 14
  3. 25
  4. 18
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 4 von 10

Worauf zeigt end nach der Konvertierung bei char *end; long n = strtol("57kg", &end, 10);?
  1. Auf das erste Zeichen von "57kg"
  2. Auf das Zeichen 7 in "57kg"
  3. Auf das Zeichen k in "57kg"
  4. Auf das abschließende Nullzeichen nach "57kg"
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 5 von 10

Ein Programm erhält die Kommandozeile ./paint amber 6 in der üblichen Anordnung einer gehosteten Umgebung. Was sind argc und argv[2][0]?
  1. 2 und 6
  2. 3 und 6
  3. 3 und '6'
  4. 2 und '6'
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 6 von 10

Welcher Parametertyp passt nach der üblichen Arraykonvertierung zu int board[2][5] eines Aufrufers?
  1. int **board
  2. int (*board)[5]
  3. int *board[5]
  4. int (*board)[2]
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 7 von 10

Was sind a, b und *p nach dem Aufruf?

void shift(int **slot, int *next) {
    **slot += 2;
    *slot = next;
    **slot += 3;
}
/* In the caller: */
int a = 5, b = 12;
int *p = &a;
shift(&p, &b);
  1. 7, 15, 15
  2. 10, 12, 12
  3. 7, 15, 7
  4. 5, 15, 15
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 8 von 10

Wie groß ist der Byteversatz von map[2][3] gegenüber map[0][0], und welchen Typ hat map + 1 bei int map[4][6] mit Ganzzahlen von 4 Byte?
  1. 36 Byte und int **
  2. 60 Byte und int **
  3. 36 Byte und int (*)[6]
  4. 60 Byte und int (*)[6]
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 9 von 10

Was sind word und der Index von p danach?

char word[] = "abcd";
char *p = word;
char saved = *p++;
*p = saved;
p += 2;
*p = 'z';
  1. "aacz", Index 3
  2. "bbcz", Index 3
  3. "aazd", Index 2
  4. "aacd", Index 4
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 10 von 10

Eine Hilfsfunktion soll einen Zeiger auf das größte Element des nicht leeren Arrays des Aufrufers zurückgeben, sodass der Aufrufer es ändern kann. Welcher Entwurf erfüllt beide Anforderungen?
  1. Den größten Wert in einen lokalen int kopieren und dessen Adresse zurückgeben.
  2. Den größten Wert in einem statischen int speichern und dessen Adresse zurückgeben.
  3. Den Index des größten Elements verfolgen und &a[index] zurückgeben.
  4. Den größten Wert verfolgen und ihn über einen const int * zurückgeben.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Set 4: Operationen auswählen und Vergleichsfunktionen zum Sortieren erstellen10 Übungsfragen

Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.

Frage 1 von 10

Was deklariert int (*filter)(int);?
  1. Eine Funktion, die int entgegennimmt und int * zurückgibt.
  2. Einen Zeiger auf eine Funktion, die int entgegennimmt und int zurückgibt.
  3. Eine Funktion, die int * entgegennimmt und int zurückgibt.
  4. Einen Zeiger auf eine Funktion, die int * entgegennimmt und int zurückgibt.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 2 von 10

Eine Funktion hat die Signatur void visit(int *a, size_t n, void (*action)(int *));, und void mark(int *x); ist verfügbar. Wie stellt ein Aufrufer mark für ein Array data mit fünf Elementen bereit?
  1. visit(data, 5, mark());
  2. visit(data, 5, *mark(data));
  3. visit(data, 5, mark(data));
  4. visit(data, 5, mark);
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 3 von 10

Welcher Wert wird result zugewiesen?

int difference(int a, int b) { return a - b; }
int product(int a, int b) { return a * b; }
int (*op)(int, int) = difference;
op = product;
int result = (*op)(7, 4);
  1. 28
  2. 3
  3. 11
  4. -3
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 4 von 10

Welcher Aufruf sortiert für int readings[8]; alle Elemente mit einer kompatiblen Rückruffunktion compare?
  1. qsort(readings, sizeof readings, 8, compare);
  2. qsort(readings, 8, sizeof readings, compare);
  3. qsort(readings, 8, sizeof readings[0], compare);
  4. qsort(readings, 8, sizeof readings[0], compare());
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 5 von 10

Welche Werte haben result und calls nach diesem Code?

int calls = 0;
int add_one(int x) { calls++; return x + 1; }
int twice(int x) { calls++; return x * 2; }
/* In a function: */
int (*ops[])(int) = {twice, add_one};
int result = ops[1](ops[0](5));
  1. 12, 2
  2. 11, 1
  3. 12, 1
  4. 11, 2
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 6 von 10

Eine Ganzzahlvergleichsfunktion muss für jeden darstellbaren int aufsteigend sortieren. Welcher Rückgabeausdruck erfüllt diese Anforderung, nachdem die Elemente nach x und y gelesen wurden?
  1. (x > y) - (x < y)
  2. (x < y) - (x > y)
  3. x - y
  4. x > y
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 7 von 10

Was ermittelt bei einem Array const char *labels[] vor dem Aufruf von strcmp die Zeichenkette, die als Vergleichsparameter const void *a übergeben wird?
  1. (const char *)a
  2. (const char *const *)a
  3. *(const char *const *)a
  4. **(const char *const *)a
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 8 von 10

Eine Tabelle int (*ops[3])(int, int) enthält gültige Funktionen an den Plätzen 0 und 2 und NULL an Platz 1. Welche Prüfbedingung erlaubt für eine vorzeichenbehaftete Ganzzahl choice den Aufruf ops[choice](8, 2) sicher?
  1. choice < 3 && ops[choice] != NULL
  2. choice >= 0 && choice < 3 && ops[choice] != NULL
  3. ops[choice] != NULL && choice >= 0 && choice < 3
  4. choice >= 0 && choice < 3
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 9 von 10

Eine Vergleichsfunktion sortiert Ganzzahlen nach ihrem Betrag und gibt für entgegengesetzte Werte gleichen Betrags null zurück. Sie sortiert {4, -4, 2, -2} mit qsort. Welches Ergebnis ist garantiert?
  1. {-2, 2, -4, 4} in genau dieser Reihenfolge.
  2. {2, -2, 4, -4} in genau dieser Reihenfolge.
  3. {-4, 4, -2, 2} in genau dieser Reihenfolge.
  4. Die beiden Werte mit Betrag 2 stehen vor den Werten mit Betrag 4.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 10 von 10

Eine Liste wird mit diesem Körper einer Rückruffunktion nach zunehmender Zeichenkettenlänge sortiert. Was ist der Fehler?

const char *x = *(const char *const *)a;
const char *y = *(const char *const *)b;
size_t nx = strlen(x), ny = strlen(y);
return nx < ny;
  1. Sie gibt für kürzeren Text einen positiven Wert und für längeren null zurück.
  2. Sie liest die Zeichenkettenlängen aus den Bytes der Zeigerplätze.
  3. Sie ändert die Zeichenketten, während sie ihre Längen misst.
  4. Sie vergleicht Zeichenadressen statt Zeichenanzahlen.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Hängst du bei deiner eigenen Aufgabe fest? Hausaufgabenhilfe

Schlüsselbegriffe

60 Schlüsselbegriffe, jeweils mit Definition

A

Adressoperator
Der unäre Adressoperator & ermittelt die Adresse eines adressierbaren Objekts oder einer Funktion und erzeugt einen passenden Zeigertyp.
Für ein Ganzzahlarray liefert &values[2] die Adresse des dritten Elements, ohne dessen Inhalt zu ändern.
Aliasing
Aliasing liegt vor, wenn unterschiedliche Ausdrücke oder Zeiger dasselbe Objekt bezeichnen, sodass ein Schreibzugriff über einen Weg über einen anderen sichtbar ist.
Nach int *q = p; erreichen beide Zeiger dieselbe Ganzzahl, bis eine spätere Zuweisung einen von ihnen neu ausrichtet.
Anpassung von Arrayparametern
Die Anpassung von Arrayparametern wandelt eine Arraydeklaration in einer Funktionsparameterliste in einen Zeigerparameter um, ohne die Arraylänge zu kopieren.
Ein als int a[10] geschriebener Parameter ist innerhalb der Funktion ein Zeiger, sodass sizeof a diesen Zeiger misst.
argc
argc ist die nicht negative Argumentanzahl, die an main in einer gehosteten Umgebung übergeben wird. Sie zählt die über argv bereitgestellten Zeichenketten einschließlich eines vorhandenen Programmnamens.
Prüfe vor dem Lesen von argv[2], ob argc > 2 gilt, damit die angeforderte Argumentzeichenkette tatsächlich existiert.
argv
argv ist die Liste von Zeichenzeigern, die an main in einer gehosteten Umgebung übergeben wird. Bei Index argc steht ein Nullzeiger, und die Zeichenketten der Benutzerargumente beginnen bei Index 1.
Bei einem üblichen Aufruf ./sum 8 13 erreicht argv[2] die Zeichenkette "13"; das Argument ist vor der Zahlenkonvertierung Text.
Array-zu-Zeiger-Konvertierung
Die Array-zu-Zeiger-Konvertierung wandelt einen Arrayausdruck in den meisten Ausdruckskontexten in einen Zeiger auf sein erstes Element um, ohne seine Elemente zu kopieren.
int *p = values; initialisiert einen Zeiger auf values[0]; das ursprüngliche Array besitzt weiterhin alle seine Elemente.
Arrayausschnitt
Ein Arrayausschnitt kann in C als Anfangszeiger und Anzahl übergeben werden, die einen ausgewählten zusammenhängenden Teil eines vorhandenen Arrays beschreiben.
Die Übergabe von data + 2 mit der Anzahl 3 wählt die ursprünglichen Indizes 2 bis 4 aus, ohne sie zu kopieren.
Arraygrenzen
Die Arraygrenzen bestimmen, welche Elementindizes existieren und wohin sich ein Zeiger bewegen darf; nur eine zusätzliche Position ist als Endmarkierung erlaubt.
Das Bilden von a - 1 für einen Zeiger am Anfang eines Arrays ist ungültig, selbst wenn das Programm nie darüber liest.
Ausgabeparameter
Ein Ausgabeparameter ist ein Zeigerparameter, über den zusätzlich zu einem Rückgabewert ein Ergebnis in beschreibbarem Speicher des Aufrufers abgelegt wird.
Eine Hilfsfunktion zur Division kann separate Ganzzahlen für Quotient und Divisionsrest befüllen und gleichzeitig einen Erfolgsstatus zurückgeben.
automatische Speicherdauer
Bei automatischer Speicherdauer existiert ein gewöhnliches lokales Objekt während der Ausführung seines Blocks; sein Speicher endet, wenn die Ausführung den Block verlässt.
Ein gewöhnliches lokales Ergebnis einer Hilfsfunktion existiert nach der Rückkehr nicht mehr, selbst wenn die Hilfsfunktion seine Adresse in einen Zeiger kopiert hat.

B

Byte
Ein Byte ist die Speichereinheit, in der sizeof misst; ein C-char belegt immer ein Byte, während die Größen anderer Typen vom System abhängen.
Auf einem Zielsystem mit Ganzzahlen von vier Byte belegt ein Array aus acht Ganzzahlen 32 Byte, bevor ein Zeiger deklariert wird.
Byte-Reihenfolge
Die Byte-Reihenfolge ist die Reihenfolge der Bytes innerhalb eines Werts aus mehreren Bytes; Little-Endian-Systeme speichern das niederwertigste Byte zuerst.
Ein 0x12345678 mit vier Byte hat auf einem Little-Endian-Zielsystem in aufsteigender Adressreihenfolge die Bytes 78 56 34 12.

D

Dereferenzierung
Bei der Dereferenzierung wird das unäre * auf einen Zeiger angewendet, um das referenzierte Objekt zu bezeichnen und so einen geeigneten Lese- oder Schreibzugriff über diesen Weg zu ermöglichen.
Nach int *p = &score; ändert die Zuweisung *p = 28; dieselbe Ganzzahl, die später über score gelesen wird.

E

Endzeiger
Ein Endzeiger hält fest, wo eine Textkonvertierung gestoppt hat, sodass der Aufrufer den nicht verarbeiteten hinteren Teil unabhängig vom numerischen Ergebnis untersuchen kann.
Die Konvertierung von "73ms" mit strtol kann 73 zurückgeben und den Endzeiger des Aufrufers auf das m setzen.

F

Funktionssignatur
Eine Funktionssignatur beschreibt die Rückgabe- und Parametertypen, die mit dem Zeigertyp kompatibel sein müssen, über den die Funktion aufgerufen wird.
Ein Zeiger für zwei Ganzzahlparameter kann eine Funktion, die einen double erwartet, auch durch eine explizite Typumwandlung nicht sicher aufrufen.
Funktionszeiger
Ein Funktionszeiger bezeichnet eine aufrufbare Funktion und hält die kompatiblen Rückgabe- und Parametertypen fest, die für einen Aufruf über ihn erforderlich sind.
int (*op)(int, int) = add; speichert eine Additionsfunktion, die später als op(4, 7) aufgerufen werden kann.

G

generischer Objektzeiger
Ein generischer Objektzeiger verwendet void * oder eine qualifizierte Form, um eine Objektadresse zu enthalten, die vor dem Zugriff wieder einen geeigneten Typ erhalten muss.
Eine Ganzzahlvergleichsfunktion wandelt const void * in const int * um, bevor sie das adressierte Arrayelement liest.

H

hängender Zeiger
Ein hängender Zeiger ist ein Zeiger, dessen früheres Ziel nicht mehr existiert, sodass er für einen späteren Zugriff auf dieses Objekt nicht mehr nutzbar ist.
Die Rückgabe der Adresse einer lokalen Variablen mit automatischer Speicherdauer hinterlässt einen hängenden Zeiger, wenn der Funktionsaufruf endet.

I

Indexoperator
Der Indexoperator greift auf ein Element zu, indem er a[i] als *(a + i) definiert und den Elementtyp des Zeigers zur Skalierung der Position verwendet.
Zeigt p auf das ursprüngliche Element 2, greift p[1] auf das ursprüngliche Element 3 desselben Arrays zu.
Indirektionsebene
Eine Indirektionsebene ist ein Zeigerschritt in einer Kette von Adressen; eine Dereferenzierung folgt ihr, und das Ermitteln einer Objektadresse fügt eine Ebene hinzu.
Bei int **pp liefert die erste Dereferenzierung einen int *, und die zweite erreicht ein Ganzzahlobjekt.

K

konstanter Zeiger
Ein konstanter Zeiger ist ein Zeigerobjekt, dessen gespeicherte Adresse nach der Initialisierung nicht geändert werden kann, selbst wenn sein Ziel beschreibbar bleibt.
Bei int *const p = &x; ist *p = 3; erlaubt, aber p = &y; kann den Zeiger nicht neu ausrichten.

N

Nullzeichen
Das Nullzeichen ist das Zeichen mit dem Wert null, geschrieben '\0', das eine C-Zeichenkette innerhalb ihres zugänglichen Zeichenspeichers abschließt.
Eine Längenschleife stoppt am abschließenden Nullzeichen, während eine vollständige Kopierschleife auch dieses letzte Zeichen schreiben muss.
Nullzeiger
Ein Nullzeiger ist ein Zeigerwert, der weder ein Objekt noch eine Funktion bezeichnet und ein optionales leeres Ergebnis oder eine fehlende Rückruffunktion darstellen kann.
Eine Elementsuche gibt NULL zurück, wenn es keinen Treffer gibt, sodass ihr Aufrufer das Ergebnis vor der Dereferenzierung prüft.

O

Operatorpriorität
Die Operatorpriorität bestimmt, wie Operatoren gruppiert werden, wenn Klammern die Gruppierung nicht festlegen, bevor der entstehende Ausdruck ausgewertet wird.
Das Postfix-++ bindet stärker als das unäre *, sodass *p++ als *(p++) und nicht als (*p)++ gruppiert wird.

P

Postfix-Inkrement
Das Postfix-Inkrement liefert den alten Wert eines Objekts an seinen umgebenden Ausdruck und erhöht dieses Objekt, wie beim Zeigerschritt in *p++.
int x = *p++; liest das Element an der alten Position und lässt p auf die nächste Position zeigen.
ptrdiff_t
ptrdiff_t ist der vorzeichenbehaftete Ganzzahltyp für die Zeigersubtraktion; das Ergebnis einer definierten Subtraktion muss in diesem Typ darstellbar sein.
Binde für ptrdiff_t die Headerdatei <stddef.h> ein und verwende %td, um eine Differenz zwischen gültigen Positionen desselben Arrays auszugeben.
Pufferüberlauf
Bei einem Pufferüberlauf wird über die Grenzen eines Zielobjekts hinaus geschrieben, etwa wenn beim Kopieren einer Zeichenkette Platz für das abschließende Nullzeichen fehlt.
Ein Wort mit drei Buchstaben benötigt vier Zeichenplätze; ein Ziel mit drei Byte kann sein abschließendes Nullzeichen nicht aufnehmen.

Q

qsort
qsort ist die Standardbibliotheksfunktion, die ein Array anhand seiner Elementanzahl, Elementgröße und einer übergebenen Vergleichsfunktion als Rückruffunktion umordnet.
Beim Sortieren von Zeichenkettenzeigerelementen wird sizeof names[0] für die Platzgröße benötigt, nicht die Zeichenanzahl einer Zeichenkette.

R

Referenzübergabe
Referenzübergabe bezeichnet in C üblicherweise die Technik, eine Adresse zu übergeben und ihr Ziel zu ändern, während die Adresse selbst kopiert wird.
Eine mit &x und &y aufgerufene Vertauschfunktion ändert Variablen des Aufrufers, indem sie über die beiden lokalen Zeigerparameter schreibt.
Rückruffunktion
Eine Rückruffunktion ist eine Funktion, die einer anderen Funktion übergeben wird, damit diese sie aufruft, wenn ihre allgemeine Aufgabe eine ausgewählte Aktion oder Entscheidung benötigt.
Ein Durchlauf nimmt einen Aktionszeiger entgegen und ruft ihn mit der Adresse jedes Elements auf, um dieses Element zu aktualisieren.

S

sizeof
Der Operator sizeof gibt die Speichergröße eines Typs oder Objekts in Bytes an und liefert einen Wert des vorzeichenlosen Ganzzahltyps size_t zurück.
Für char code[10] = "go"; gibt sizeof code den Wert 10 an, obwohl der Text nur zwei Zeichen hat.
Speicheradresse
Eine Speicheradresse bezeichnet ein Byte im Speicher; die Adresse eines Objekts bezeichnet das erste Byte des Speichers, den es belegt.
Beginnt eine Ganzzahl mit vier Byte an der Beispieladresse 0x2000, folgen ihre übrigen Bytes bis 0x2003.
Speicherausrichtung
Die Speicherausrichtung ist die Einschränkung eines Typs für geeignete Anfangsadressen seiner Objekte; bei gewöhnlichen Deklarationen wird sie automatisch berücksichtigt.
Bei einer verbreiteten Anordnung von Ganzzahlen mit einer Ausrichtung auf vier Byte liegen aufeinanderfolgende Arrayelemente an durch vier teilbaren Adressen.
Speicherdauer
Die Speicherdauer beschreibt, wie lange der Speicher eines Objekts besteht, etwa für eine Blockausführung bei einer gewöhnlichen lokalen Variablen mit automatischer Speicherdauer.
Ein aus einem inneren Block gespeicherter Zeiger kann die gewöhnliche lokale Ganzzahl dieses Blocks danach nicht am Leben halten.
Speicherzugriffsverletzung
Eine Speicherzugriffsverletzung ist ein vom System gemeldeter ungültiger Speicherzugriff, der häufig ein Programm stoppt, obwohl ein ungültiger C-Zugriff nicht dazu führen muss.
Die Dereferenzierung eines Nullzeigers kann mit einer Speicherzugriffsverletzung stoppen; ein anderer ungültiger Zeigerschreibzugriff kann unbemerkt Daten beschädigen.
Sprungtabelle
Eine Sprungtabelle ist ein Array von Funktionszeigern, das über einen zur Laufzeit gewählten Index eine Operation auswählt.
Ein Operationsmenü prüft seinen Auswahlindex, bevor es damit einen Tabelleneintrag abruft und aufruft.
stabile Sortierung
Eine stabile Sortierung erhält die ursprüngliche relative Reihenfolge von Elementen, die ihre Vergleichsfunktion als gleich behandelt; qsort bietet keine solche Garantie.
Das Sortieren von Namen allein nach Länge kann zwei gleich lange Namen umordnen, obwohl ihr Vergleichsergebnis null ist.
Stackframe
Ein Stackframe ist der Speicher, den eine typische Implementierung für die Arbeitsdaten eines Funktionsaufrufs verwendet, einschließlich vieler lokaler Objekte mit automatischer Speicherdauer.
Nach der Rückkehr aus einer Funktion kann der Speicher ihres alten Stackframes wiederverwendet werden; eine gespeicherte lokale Adresse kann ihn nicht erhalten.
statische Speicherdauer
Bei statischer Speicherdauer besteht der Speicher eines Objekts während des gesamten Programms; eine lokale statische Variable behält ihren Wert zwischen Aufrufen.
Zwei Zeiger, die auf dasselbe statische Ergebnisobjekt zurückgegeben werden, sehen denselben aktualisierten Wert, nachdem ein weiterer Aufruf ihn geschrieben hat.
strtol
strtol ist die Standardbibliotheksfunktion, die führenden numerischen Text in long umwandelt und ihre Stoppposition über char ** melden kann.
Die Übergabe von &end ermöglicht strtol, den Zeichenzeiger des Aufrufers durch die Adresse des ersten nicht verwendeten Zeichens zu ersetzen.

T

typedef
Eine typedef-Deklaration führt einen Namen für einen vorhandenen Typ ein und erleichtert so die Wiederverwendung eines komplexen Funktionszeigertyps in Deklarationen.
Nach typedef int (*operation)(int); deklariert operation chosen; einen Funktionszeiger, keinen Funktionskörper.

U

unbestimmter Zeigerwert
Ein unbestimmter Zeigerwert ist der nicht initialisierte Zustand eines gewöhnlichen lokalen Zeigers; er liefert kein bekanntes nutzbares Ziel.
Die Deklaration int *p; allein erlaubt dem Programm nicht, diesen Zeiger sicher zu prüfen, ihm zu folgen oder über ihn zu schreiben.
undefiniertes Verhalten
Undefiniertes Verhalten bedeutet, dass der C-Standard keine Anforderungen an das Ergebnis einer Operation stellt, auch nicht daran, ob das Programm stoppt oder weiterläuft.
Die Dereferenzierung eines Nullzeigers ist undefiniertes Verhalten, selbst wenn eine bestimmte Maschine eine Speicherzugriffsverletzung meldet.
unregelmäßiges Array
Ein unregelmäßiges Array ist eine Sammlung, deren Zeilen unterschiedlich lang sein können und die in C häufig durch Zeiger auf getrennt gespeicherte Zeilen dargestellt wird.
Zeichenkettenzeiger können auf "red" und "violet" verweisen, ohne für beide Wörter gleich große Zeichenzeilen zu reservieren.

Ü

Überlauf bei vorzeichenbehafteten Ganzzahlen
Ein Überlauf bei vorzeichenbehafteten Ganzzahlen tritt auf, wenn ein vorzeichenbehaftetes arithmetisches Ergebnis außerhalb des darstellbaren Wertebereichs seines Typs liegt; in C ist dies undefiniertes Verhalten.
Eine Ganzzahlvergleichsfunktion mit x - y kann überlaufen, selbst wenn jede Eingabe einzeln in einen int passt.

V

Vergleichsfunktion
Eine Vergleichsfunktion ordnet ein Elementpaar anhand eines negativen, nullwertigen oder positiven Ergebnisses ein: erstes davor, gleichwertig oder erstes danach.
Die Rückgabe von (x > y) - (x < y) ordnet Ganzzahlen aufsteigend, ohne die Werte selbst zu subtrahieren.

W

Wertübergabe
Bei der Wertübergabe erhält ein Funktionsparameter eine Kopie des Werts seines Arguments, auch wenn dieser Wert ein Zeiger auf Speicher des Aufrufers ist.
Eine Zuweisung an einen lokalen int-Parameter lässt die Ganzzahl des Aufrufers unverändert; ein Schreibzugriff über eine kopierte Adresse kann sie ändern.

Z

Zeichenkettenliteral
Ein Zeichenkettenliteral ist Text in Anführungszeichen in C, der ein Array mit statischer Speicherdauer bereitstellt, dessen Zeichen nicht verändert werden dürfen.
const char *p = "oak"; macht das Literal über einen schreibgeschützten Weg zugänglich, während char a[] = "oak"; eine beschreibbare Kopie erstellt.
Zeiger
Ein Zeiger ist ein typisierter Wert, der ein Objekt oder eine Funktion bezeichnen kann; ein Objektzeiger kann auch einen Nullwert oder eine Position unmittelbar hinter dem Ende enthalten.
In int *p = &count; speichert die Zeigervariable eine Adresse, während die Ganzzahl ihren eigenen Wert behält.
Zeiger auf ein Array
Ein Zeiger auf ein Array bezeichnet ein ganzes Array als seinen Elementtyp, sodass die Addition von 1 um die Speichergröße dieses vollständigen Arrays vorrückt.
Für int a[4] hat &a den Typ int (*)[4]; seine Schrittweite unterscheidet sich von der von &a[0].
Zeiger auf einen Zeiger
Ein Zeiger auf einen Zeiger bezeichnet ein Zeigerobjekt und fügt eine Indirektionsebene hinzu, bevor der Zugriff die endgültigen referenzierten Daten erreicht.
Bei int **pp = &p; richtet die Zuweisung *pp = &y; den Zeiger p neu aus, während **pp = 8; sein Ziel ändert.
Zeiger auf konstante Daten
Ein Zeiger auf konstante Daten erlaubt das Lesen seines Ziels, lehnt aber Schreibzugriffe über diesen Zeiger ab; der Zeiger selbst darf weiterhin neu ausgerichtet werden.
const int *p = &x; erlaubt p = &y;, während eine Zuweisung wie *p = 3; abgelehnt wird.
Zeiger unmittelbar hinter das letzte Arrayelement
Ein Zeiger unmittelbar hinter das letzte Arrayelement markiert die Position direkt nach dem letzten Element eines Arrays und darf als Schranke dienen, aber nicht dereferenziert werden.
Für int a[5] kann ein Durchlauf stoppen, wenn sein aktueller Zeiger gleich a + 5 ist, bevor auf ein weiteres Element zugegriffen wird.
Zeigerarithmetik
Die Zeigerarithmetik versetzt einen Zeiger um eine Anzahl von Elementen seines Typs; Ergebnisse sind auf sein Array und dessen Position unmittelbar hinter dem Ende beschränkt.
Die Addition von 2 zu einem Ganzzahlzeiger versetzt ihn um zwei Ganzzahlpositionen; der Aufrufer muss den Versatz nicht mit sizeof skalieren.
Zeigerarray
Ein Zeigerarray speichert separate Adresswerte als seine Elemente, während die referenzierten Objekte an anderen Speicherorten liegen.
Eine Liste aus vier Zeichenkettenzeigern hat vier Adressplätze, selbst wenn die Zeichenketten sehr unterschiedlich lang sind.
Zeigerdeklarator
Ein Zeigerdeklarator verwendet * mit einem einzelnen deklarierten Namen, um anzugeben, dass das benannte Objekt einen Zeigertyp hat.
In int *left, right; ist nur left ein Zeiger; um right als Zeiger zu deklarieren, wird ein weiteres Sternchen benötigt.
Zeigerdifferenz
Eine Zeigerdifferenz ist die vorzeichenbehaftete Elementanzahl, die bei der Subtraktion zweier Positionen in einem Array entsteht, einschließlich einer gültigen Position unmittelbar hinter dem Ende.
&a[5] - &a[2] ergibt 3 Elemente, unabhängig davon, wie viele Bytes ein Element auf diesem System belegt.
Zeigerparameter
Ein Zeigerparameter ist ein lokaler Zeiger einer Funktion, der aus einem Argument initialisiert wird und der Funktion Zugriff auf ein Objekt über dessen Adresse gibt.
Eine Reset-Hilfsfunktion erhält &score über int *out und schreibt mit *out null in die Ganzzahl des Aufrufers.
zeilenweise Speicherung
Bei der zeilenweisen Speicherung stehen in einem rechteckigen Array alle Elemente einer Zeile vor denen der nächsten; die feste Zeilenbreite bestimmt die Schrittweite zwischen Zeilen.
Bei int a[3][4] rückt die Addition von 1 zum konvertierten a um eine ganze Zeile aus vier Ganzzahlen vor, nicht um eine Ganzzahl.
zweidimensionales Array
Ein zweidimensionales Array ist ein Array, dessen Elemente Arrays fester Größe sind. Es speichert vollständige zusammenhängende Zeilen statt separater Zeilenzeiger.
int grid[2][5] wird in int (*)[5] umgewandelt; eine Übergabe als int ** würde eine andere Anordnung beschreiben.

Über diese Einheit

Von StudeliFehler melden

Erstelle deine eigenen Lernmaterialien

Mach aus deinen Notizen, Folien und Dokumenten Lernleitfäden, Übungsfragen, Karteikarten und 6 weitere Tools.

Jetzt starten