- Lernen
- Informatik & Informationswissenschaften
- Programmierung
- C-Programmierung – Zeiger, Speicherverwaltung und sicheres C
- Zeiger und Speicheradressen in C
Zeiger und Speicheradressen in C
&, 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
- 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 veranschaulichenBeschreibe 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 & ermittelnDeklariere 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 ändernVerwende 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.
- Die Anordnung von Variablen im Speicher veranschaulichen
- Schreibe Funktionen, die über Zeigerparameter Variablen ihres Aufrufers ändern, schütze Daten, die nur gelesen werden sollen, mit
constund gib Zeiger zurück, die gültig bleiben.- Variablen über Zeigerparameter als Referenz übergebenErklä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
scanfzu lesen. - Daten mit const schützen und Zeiger sicher zurückgebenKennzeichne 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.
- Variablen über Zeigerparameter als Referenz übergeben
- 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 rechnenAddiere Ganzzahlen zu Zeigern, subtrahiere und vergleiche Zeiger in dasselbe Array und erkenne, welche Ergebnisse gültig sind.
- Den Zusammenhang zwischen Arrays und Zeigern erklärenNutze 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 durchlaufenSchreibe Schleifen, die Arrays und nullterminierte Zeichenketten mit Zeigern durchlaufen, verwende gebräuchliche Ausdrücke wie
*p++korrekt und halte die Grenzen ein.
- Mit Zeigern rechnen
- Verfolge mehrere Indirektionsebenen, lass Funktionen einen Zeiger des Aufrufers aktualisieren und verwende Zeigerarrays wie die Kommandozeilenargumente.
- Zeiger auf Zeiger verwendenDeklariere 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 verwendenErstelle Arrays von Zeichenkettenzeigern, verarbeite
argcundargvals Zeiger und unterscheide ein Zeigerarray von einem zweidimensionalen Array.
- Zeiger auf Zeiger verwenden
- Speichere und rufe Funktionen über Zeiger auf, übergib Funktionen an andere Funktionen und sortiere Daten beliebigen Typs mit dem standardmäßigen
qsortund einer Vergleichsfunktion als Rückruffunktion.- Funktionszeiger deklarieren, ihnen Werte zuweisen und Funktionen über sie aufrufenSchreibe 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ürqsort.
- Funktionszeiger deklarieren, ihnen Werte zuweisen und Funktionen über sie aufrufen
Zeiger und Speicheradressen in C einfach erklärt
1Speicher, 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:
| Element | Erstes Byte | Letztes Byte |
|---|---|---|
scores[0] | 0x1000 | 0x1003 |
scores[1] | 0x1004 | 0x1007 |
scores[2] | 0x1008 | 0x100B |
scores[3] | 0x100C | 0x100F |
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
sizeofmisst Bytes;strlenzä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
| Deklaration | Bedeutung |
|---|---|
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. &countist eine Adresse;countist 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
%pund einem Argument vom Typvoid *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 Zustand | Wirkung |
|---|---|
*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
*pbezeichnet das überperreichte Objekt.- Das Kopieren einer Adresse erzeugt Aliase; das Kopieren von
*pkopiert 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.
2Funktionen 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);
azeigt aufxundbaufy.tspeichert den ursprünglichen Wert vonx, also 3.*a = *bschreibt 8 inx.*b = tschreibt die gespeicherte 3 iny.
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
*aund*bmithilfe eines gespeicherten Werts. - Für Ausgabeparameter muss der Aufrufer gültige beschreibbare Objekte bereitstellen.
scanferhä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
| Deklaration | Darf p neu ausgerichtet werden? | Darf über *p zugewiesen werden? |
|---|---|---|
const int *p = &x; | Ja | Nein |
int *const p = &x; | Nein | Ja |
const int *const p = &x; | Nein | Nein |
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ährendint *constdie Adresse festlegt.- Ein mit
constdefiniertes 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.
3Arrays 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:
| Zeigertyp | Größe eines Elements | Adresse nach p + 3 |
|---|---|---|
int * | 4 Byte | 0x100C |
double * | 8 Byte | 0x1018 |
char * | 1 Byte | 0x1003 |
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 Byte | Ergebnis |
|---|---|
sizeof a | 20 Byte für alle fünf Elemente |
sizeof p | 8 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 amisst das gesamte Array nur dort, woatatsächlich ein Array ist.- Ein Arrayparameter wird zu einem Zeiger angepasst; übergib seine nutzbare Länge separat.
&azeigt 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.
4Einem 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.
| Ausdruck | Typ | Erreichtes Objekt oder erreichter Wert |
|---|---|---|
pp | int ** | Adresse von p |
*pp | int * | Der in p gespeicherte Zeiger |
**pp | int | Die 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 **ppkann die Adresse einesint *-Objekts enthalten.- Jede Dereferenzierung folgt einer Zeigerebene.
- Über
**pplässt sich die endgültige Ganzzahl ändern; über*pplässt sich der dazwischenliegende Zeiger ändern. - Um den Zeiger eines Aufrufers zu aktualisieren, wird die Adresse dieses Zeigers benötigt.
strtolverwendetchar **, 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 Ausdruck | Bedeutung |
|---|---|
int (*rows)[4] | Zeiger auf eine Zeile aus vier Ganzzahlen |
int *ptrs[4] | Array aus vier Ganzzahlzeigern |
Konvertiertes grid | int (*)[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]undint **beschreiben unterschiedliche Speicheranordnungen.- Zwei Indizes allein belegen nicht, dass zwei Objekte denselben Typ haben.
5Funktionen 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ückgabewerts | Reihenfolge |
|---|---|
| Negativ | Erstes vor zweitem |
| Null | Für diese Reihenfolge gleichwertig |
| Positiv | Erstes 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 Ordnungsausdruck 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.
qsortbenö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
qsortkeine stabile Reihenfolge.
Zeiger und Speicheradressen in C: Beispiele
1Eine Adresse und den darüber erreichten Wert verfolgen
13 Schritte
- 1Einfü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?
- 2ErklärungVier Ganzzahlen anordnen
Stell dir vor,
int scores[4]beginnt auf einem System mit Ganzzahlen von 4 Byte bei0x1000. Das erste Element belegt die Bytes0x1000bis0x1003. Das nächste beginnt bei0x1004, gefolgt von0x1008und0x100C.Die Elemente liegen aufeinanderfolgend im Speicher. Diese Anordnungsregel gilt für das Array; sie garantiert keine Reihenfolge für zwei getrennt deklarierte Ganzzahlen.
- 3BeispielDas Array und dann seinen Inhalt messen
Für
char word[20] = "hi";ergeben zwei Messungen unterschiedliche Ergebnisse:sizeof wordergibt 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.
sizeofgibt einen Wert vom Typsize_tzurück, der mit%zuausgegeben wird; es zählt nicht, wie viele Elemente das Programm derzeit verwendet. - 4VerbindungAnordnung 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.
- 5HerausforderungDie 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. - 6ErklärungDem Zeiger ein Ziel geben
Betrachte
int count = 42; int *p = &count;. Der Operator&ermittelt die Adresse voncount, undpspeichert diese Adresse. Liegtcountan der Beispieladresse0x2000, ist der Wert des Zeigers0x2000; der Wert der Ganzzahl bleibt 42.Der Zeiger belegt auch eigenen Speicher. Seine Adresse ist
&p; das ist eine andere Information als die inpenthaltene Adresse. - 7TippDas Sternchen neben jedem Namen lesen
int *p, q;deklariert einen Zeiger und eine gewöhnliche Ganzzahl. Um zwei Zeiger zu deklarieren, verwendeint *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. - 8HerausforderungNeu ausrichten, ohne Daten zu ändern
Beginne mit
int a = 9, b = 16; int *p = &a;. Führe dannp = &b;aus. Welche Ganzzahlwerte ändern sich und was enthältpnun?---reveal---
Keine der beiden Ganzzahlen ändert sich:
ableibt 9 undbbleibt 16. Nurpändert sich und speichert die Adresse vonb. Eine Zeigerzuweisung ersetzt eine Adresse; sie kopiert weder den Inhalt des alten noch den des neuen Ziels. - 9BeispielÜber p lesen, addieren und schreiben
Fahre mit
int x = 10; int *p = &x;fort und führe aus:*p = *p + 5;Das
*pauf der rechten Seite liest 10 ausx. Die Addition von 5 ergibt 15. Das*pauf der linken Seite bezeichnet dasselbe Objekt, sodass die Zuweisung 15 inxschreibt. Die Adresse inphat sich nicht geändert. Anschließend liestint y = *p * 2;den Wert 15 und erstellt eine separate Ganzzahly, die 30 enthält. - 10KernaussageEine Adresskopie schafft einen weiteren Weg
Bei
int n = 6; int *p = &n; int *q = p;erreichen beide Zeiger die Variablen. Die Zuweisung hat eine Adresse kopiert, nicht das Ganzzahlobjekt.Nach
*q = 14;ergibt das Lesen vonn,*poder*qden Wert 14. Zeichne ein Ganzzahlkästchen und zwei Pfeile dorthin. Zwei Ganzzahlkästchen würden fälschlich unabhängige Daten nahelegen. - 11TippDas 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.
- 12HerausforderungEiner kopierten Adresse folgen
Was sind die endgültigen Werte von
aundb?int a = 6, b = 19; int *p = &a, *q = p; p = &b; *q = *p + 1;---reveal---
awird zu 20 undbbleibt 19. Das Neuausrichten vonplässtqweiterhin aufazeigen. Die letzte Zeile liestbüberpund schreibt die Summe überqnacha. - 13ZusammenfassungDas 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.
2Eine Funktion Daten des Aufrufers ändern lassen
11 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungDer Adresse in einen Aufruf folgen
- 2ErklärungAuch eine kopierte Adresse erreicht den Aufrufer
- 3BeispielDen Wert vor dem Ersetzen speichern
- 4BeispielDen zweiten Wert an die erste Stelle schreiben
- 5BeispielDie Vertauschung abschließen
- 6HerausforderungZwei Ausgaben liefern
- 7Erklärungconst auf die vorgesehene Ebene setzen
- 8BeispielEine Position in einem noch existierenden Objekt zurückgeben
- 9HerausforderungDas Problem mit der Lebensdauer erklären
- 10HerausforderungEine beschreibbare Suchschnittstelle wählen
- 11ZusammenfassungSchreibzugriff und Lebensdauer prüfen
3Ein Array durchlaufen, ohne seine Grenze zu überschreiten
15 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungAnfang, Schritt und Ende finden
- 2ErklärungUm Elemente weitergehen
- 3BeispielZwei Positionen subtrahieren
- 4KernaussageEine Schranke verwenden, die kein Element ist
- 5HerausforderungDie Mitte bestimmen
- 6ErklärungEin Arrayausdruck liefert einen Anfangszeiger
- 7Beispielsizeof an der richtigen Stelle anwenden
- 8HerausforderungEinen kleineren Bereich übergeben
- 9BeispielErst lesen und dann weitergehen oder umgekehrt
- 10ErklärungDas Ende einer Zeichenkette finden
- 11BeispielZeichen für Zeichen kopieren
- 12TippDen Platz für das abschließende Nullzeichen reservieren
- 13HerausforderungDen letzten Schleifenbesuch korrigieren
- 14HerausforderungEine sichere Vorbereitung für das Umkehren wählen
- 15ZusammenfassungJede besuchte Position ausdrücklich bestimmen
4Einen Zeiger ändern und ein Zeigerarray lesen
12 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungDen Zeiger selbst zum Ziel machen
- 2BeispielZwei Pfeile zeichnen
- 3ErklärungWählen, welche Ebene sich ändert
- 4BeispielDie Position des Aufrufers über eine Adresse übergeben
- 5HerausforderungEine Änderung nur am Zeiger vorhersagen
- 6VerbindungDie Stoppposition einer Konvertierung lesen
- 7ErklärungEin Array kann Zeigerplätze besitzen
- 8BeispielDurch Adressen weitergehen
- 9TippArgumentanzahl vor dem Zeichenkettenzugriff prüfen
- 10HerausforderungEinen Typ für eine ganze Zeile lesen
- 11HerausforderungEine Funktion an den bereitgestellten Speicher anpassen
- 12ZusammenfassungDas Objekt auf jeder Ebene benennen
5Eine Operation auswählen und über eine Rückruffunktion sortieren
15 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungEine allgemeine Aufgabe von ihrer ausgewählten Operation trennen
- 2ErklärungDen aufrufbaren Typ lesen
- 3BeispielEine andere Funktion auswählen
- 4TippEinen Zeigertyp für eine Tabelle benennen
- 5HerausforderungEine verwandte Deklaration lesen
- 6ErklärungEine Aktion als Rückruffunktion bereitstellen
- 7BeispielDie vier Sortierargumente bereitstellen
- 8ErklärungDen Ganzzahlelementtyp zurückgewinnen
- 9BeispielDas Vorzeichen der Reihenfolge zurückgeben
- 10HerausforderungEine überlaufende Abkürzung vermeiden
- 11VerbindungZeichenkettenzeiger statt Zeichenzeilen sortieren
- 12HerausforderungDie richtige Sortiergröße wählen
- 13KernaussageGleichheit verspricht keine Stabilität
- 14HerausforderungEine ausgewählte Vergleichsfunktion verfolgen
- 15ZusammenfassungAufruf, Anordnung und Reihenfolge aufeinander abstimmen
Zeiger und Speicheradressen in C: Übungsaufgaben
Set 1: Adressen und Funktionsaufrufe verfolgen10 Übungsfragen
Frage 1 von 10
int belegt 4 Byte. Ein Array int levels[7] beginnt an der Beispieladresse 0x2400. An welcher Adresse beginnt levels[4]?Antwort zeigenAntwort ausblenden
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
int items = 16; einen Zeiger auf items?Antwort zeigenAntwort ausblenden
&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;
Antwort zeigenAntwort ausblenden
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);
Antwort zeigenAntwort ausblenden
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);
Antwort zeigenAntwort ausblenden
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);
Antwort zeigenAntwort ausblenden
*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
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?Antwort zeigenAntwort ausblenden
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
int x = 6, y = 17; const int *p = &x; erlaubt?Antwort zeigenAntwort ausblenden
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;
Antwort zeigenAntwort ausblenden
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);
Antwort zeigenAntwort ausblenden
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
&values[6] - &values[2] für double values[8];?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 2 von 10
puts(message + 5) für const char *message = "blue sky"; aus?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;
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};
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};
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)++;
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;
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;
}
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 9 von 10
int a[6] beginnt an der Beispieladresse 0x3000, und jede Ganzzahl belegt 4 Byte. Welche Adressen stellen a + 1 und &a + 1 dar?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++;
}
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
*pp nach int n = 31; int *p = &n; int **pp = &p;?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"};
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;
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 4 von 10
end nach der Konvertierung bei char *end; long n = strtol("57kg", &end, 10);?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 5 von 10
./paint amber 6 in der üblichen Anordnung einer gehosteten Umgebung. Was sind argc und argv[2][0]?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 6 von 10
int board[2][5] eines Aufrufers?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);
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 8 von 10
map[2][3] gegenüber map[0][0], und welchen Typ hat map + 1 bei int map[4][6] mit Ganzzahlen von 4 Byte?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';
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 10 von 10
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
int (*filter)(int);?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 2 von 10
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?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);
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 4 von 10
int readings[8]; alle Elemente mit einer kompatiblen Rückruffunktion 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));
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 6 von 10
int aufsteigend sortieren. Welcher Rückgabeausdruck erfüllt diese Anforderung, nachdem die Elemente nach x und y gelesen wurden?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 7 von 10
const char *labels[] vor dem Aufruf von strcmp die Zeichenkette, die als Vergleichsparameter const void *a übergeben wird?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 8 von 10
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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 9 von 10
{4, -4, 2, -2} mit qsort. Welches Ergebnis ist garantiert?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;
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Hängst du bei deiner eigenen Aufgabe fest? Hausaufgabenhilfe
Schlüsselbegriffe
A
Adressoperator
& ermittelt die Adresse eines adressierbaren Objekts oder einer Funktion und erzeugt einen passenden Zeigertyp.&values[2] die Adresse des dritten Elements, ohne dessen Inhalt zu ändern.Aliasing
int *q = p; erreichen beide Zeiger dieselbe Ganzzahl, bis eine spätere Zuweisung einen von ihnen neu ausrichtet.Anpassung von Arrayparametern
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.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../sum 8 13 erreicht argv[2] die Zeichenkette "13"; das Argument ist vor der Zahlenkonvertierung Text.Array-zu-Zeiger-Konvertierung
int *p = values; initialisiert einen Zeiger auf values[0]; das ursprüngliche Array besitzt weiterhin alle seine Elemente.Arrayausschnitt
data + 2 mit der Anzahl 3 wählt die ursprünglichen Indizes 2 bis 4 aus, ohne sie zu kopieren.Arraygrenzen
a - 1 für einen Zeiger am Anfang eines Arrays ist ungültig, selbst wenn das Programm nie darüber liest.Ausgabeparameter
automatische Speicherdauer
B
Byte
sizeof misst; ein C-char belegt immer ein Byte, während die Größen anderer Typen vom System abhängen.Byte-Reihenfolge
0x12345678 mit vier Byte hat auf einem Little-Endian-Zielsystem in aufsteigender Adressreihenfolge die Bytes 78 56 34 12.D
Dereferenzierung
* auf einen Zeiger angewendet, um das referenzierte Objekt zu bezeichnen und so einen geeigneten Lese- oder Schreibzugriff über diesen Weg zu ermöglichen.int *p = &score; ändert die Zuweisung *p = 28; dieselbe Ganzzahl, die später über score gelesen wird.E
Endzeiger
"73ms" mit strtol kann 73 zurückgeben und den Endzeiger des Aufrufers auf das m setzen.F
Funktionssignatur
Funktionszeiger
int (*op)(int, int) = add; speichert eine Additionsfunktion, die später als op(4, 7) aufgerufen werden kann.G
generischer Objektzeiger
void * oder eine qualifizierte Form, um eine Objektadresse zu enthalten, die vor dem Zugriff wieder einen geeigneten Typ erhalten muss.const void * in const int * um, bevor sie das adressierte Arrayelement liest.H
hängender Zeiger
I
Indexoperator
a[i] als *(a + i) definiert und den Elementtyp des Zeigers zur Skalierung der Position verwendet.p auf das ursprüngliche Element 2, greift p[1] auf das ursprüngliche Element 3 desselben Arrays zu.Indirektionsebene
int **pp liefert die erste Dereferenzierung einen int *, und die zweite erreicht ein Ganzzahlobjekt.K
konstanter Zeiger
int *const p = &x; ist *p = 3; erlaubt, aber p = &y; kann den Zeiger nicht neu ausrichten.N
Nullzeichen
'\0', das eine C-Zeichenkette innerhalb ihres zugänglichen Zeichenspeichers abschließt.Nullzeiger
NULL zurück, wenn es keinen Treffer gibt, sodass ihr Aufrufer das Ergebnis vor der Dereferenzierung prüft.O
Operatorpriorität
++ bindet stärker als das unäre *, sodass *p++ als *(p++) und nicht als (*p)++ gruppiert wird.P
Postfix-Inkrement
*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.ptrdiff_t die Headerdatei <stddef.h> ein und verwende %td, um eine Differenz zwischen gültigen Positionen desselben Arrays auszugeben.Pufferüberlauf
Q
qsort
qsort ist die Standardbibliotheksfunktion, die ein Array anhand seiner Elementanzahl, Elementgröße und einer übergebenen Vergleichsfunktion als Rückruffunktion umordnet.sizeof names[0] für die Platzgröße benötigt, nicht die Zeichenanzahl einer Zeichenkette.R
Referenzübergabe
&x und &y aufgerufene Vertauschfunktion ändert Variablen des Aufrufers, indem sie über die beiden lokalen Zeigerparameter schreibt.Rückruffunktion
S
sizeof
sizeof gibt die Speichergröße eines Typs oder Objekts in Bytes an und liefert einen Wert des vorzeichenlosen Ganzzahltyps size_t zurück.char code[10] = "go"; gibt sizeof code den Wert 10 an, obwohl der Text nur zwei Zeichen hat.Speicheradresse
0x2000, folgen ihre übrigen Bytes bis 0x2003.Speicherausrichtung
Speicherdauer
Speicherzugriffsverletzung
Sprungtabelle
stabile Sortierung
qsort bietet keine solche Garantie.Stackframe
statische Speicherdauer
strtol
strtol ist die Standardbibliotheksfunktion, die führenden numerischen Text in long umwandelt und ihre Stoppposition über char ** melden kann.&end ermöglicht strtol, den Zeichenzeiger des Aufrufers durch die Adresse des ersten nicht verwendeten Zeichens zu ersetzen.T
typedef
typedef-Deklaration führt einen Namen für einen vorhandenen Typ ein und erleichtert so die Wiederverwendung eines komplexen Funktionszeigertyps in Deklarationen.typedef int (*operation)(int); deklariert operation chosen; einen Funktionszeiger, keinen Funktionskörper.U
unbestimmter Zeigerwert
int *p; allein erlaubt dem Programm nicht, diesen Zeiger sicher zu prüfen, ihm zu folgen oder über ihn zu schreiben.undefiniertes Verhalten
unregelmäßiges Array
"red" und "violet" verweisen, ohne für beide Wörter gleich große Zeichenzeilen zu reservieren.Ü
Überlauf bei vorzeichenbehafteten Ganzzahlen
x - y kann überlaufen, selbst wenn jede Eingabe einzeln in einen int passt.V
Vergleichsfunktion
(x > y) - (x < y) ordnet Ganzzahlen aufsteigend, ohne die Werte selbst zu subtrahieren.W
Wertübergabe
int-Parameter lässt die Ganzzahl des Aufrufers unverändert; ein Schreibzugriff über eine kopierte Adresse kann sie ändern.Z
Zeichenkettenliteral
const char *p = "oak"; macht das Literal über einen schreibgeschützten Weg zugänglich, während char a[] = "oak"; eine beschreibbare Kopie erstellt.Zeiger
int *p = &count; speichert die Zeigervariable eine Adresse, während die Ganzzahl ihren eigenen Wert behält.Zeiger auf ein Array
int a[4] hat &a den Typ int (*)[4]; seine Schrittweite unterscheidet sich von der von &a[0].Zeiger auf einen Zeiger
int **pp = &p; richtet die Zuweisung *pp = &y; den Zeiger p neu aus, während **pp = 8; sein Ziel ändert.Zeiger auf konstante Daten
const int *p = &x; erlaubt p = &y;, während eine Zuweisung wie *p = 3; abgelehnt wird.Zeiger unmittelbar hinter das letzte Arrayelement
int a[5] kann ein Durchlauf stoppen, wenn sein aktueller Zeiger gleich a + 5 ist, bevor auf ein weiteres Element zugegriffen wird.Zeigerarithmetik
sizeof skalieren.Zeigerarray
Zeigerdeklarator
* mit einem einzelnen deklarierten Namen, um anzugeben, dass das benannte Objekt einen Zeigertyp hat.int *left, right; ist nur left ein Zeiger; um right als Zeiger zu deklarieren, wird ein weiteres Sternchen benötigt.Zeigerdifferenz
&a[5] - &a[2] ergibt 3 Elemente, unabhängig davon, wie viele Bytes ein Element auf diesem System belegt.Zeigerparameter
&score über int *out und schreibt mit *out null in die Ganzzahl des Aufrufers.zeilenweise Speicherung
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
int grid[2][5] wird in int (*)[5] umgewandelt; eine Übergabe als int ** würde eine andere Anordnung beschreiben.Über diese Einheit
Erstelle deine eigenen Lernmaterialien
Mach aus deinen Notizen, Folien und Dokumenten Lernleitfäden, Übungsfragen, Karteikarten und 6 weitere Tools.