Vorwort: Erklärung komplexer Typen
Um Zeiger zu verstehen, werden Ihnen zwangsläufig einige relativ komplexe Typen begegnen. Daher möchte ich zunächst vorstellen, wie man einen komplexen Typ vollständig versteht. Tatsächlich ist das Verstehen komplexer Typen sehr einfach: Ein Typ enthält viele Operatoren, die, genau wie gewöhnliche Ausdrücke, eine Rangfolge haben. Ihre Rangfolge ist dieselbe wie die Operatorrangfolge. Deshalb habe ich folgendes Prinzip zusammengefasst: Ausgehend vom Variablennamen kombiniert man gemäß der Operatorrangfolge und analysiert Schritt für Schritt.
Im Folgenden beginnen wir mit einfachen Typen und analysieren sie nach und nach.
- int p;-- Dies ist eine gewöhnliche Integer-Variable.
- int *p;-- Zuerst, von p ausgehend, wird zuerst mit `*` kombiniert.*Dies zeigt an, dass p ein Zeiger ist; dann wird mit int kombiniert, was anzeigt, dass der Typ des Inhalts, auf den der Zeiger zeigt, int ist. p ist also ein Zeiger, der Integer-Daten zurückgibt.
- int p[3]-- Zuerst, von p ausgehend, wird zuerst mit `[]` kombiniert.[]Dies zeigt an, dass p ein Array ist; dann wird mit int kombiniert, was anzeigt, dass die Elemente im Array Ganzzahlen sind. p ist also ein Array, das aus Integer-Daten besteht.
- int *p[3];-- Zuerst, von p ausgehend, wird zuerst mit [] kombiniert, weil seine Priorität höher ist als*`*`, also ist p ein Array; dann wird mit * kombiniert, was anzeigt, dass die Elemente im Array Zeiger sind; dann wird mit int kombiniert, was anzeigt, dass der Typ des Inhalts, auf den die Zeiger zeigen, integer ist. p ist also ein Array, das aus Zeigern besteht, die Integer-Daten zurückgeben.
- int (*p)[3];-- Zuerst, von p ausgehend, wird zuerst mit * kombiniert, was anzeigt, dass p ein Zeiger ist; dann wird mit [] kombiniert (der Schritt "()" kann ignoriert werden; er dient nur dazu, die Priorität zu ändern), was anzeigt, dass der Inhalt, auf den der Zeiger zeigt, ein Array ist; dann wird mit int kombiniert, was anzeigt, dass die Elemente im Array Ganzzahlen sind. p ist also ein Zeiger auf ein Array, das aus Integer-Daten besteht.
- int **p;-- Zuerst, von p ausgehend, wird zuerst mit * kombiniert, was besagt, dass p ein Zeiger ist; dann wird erneut mit * kombiniert, was anzeigt, dass das Element, auf das der Zeiger zeigt, ein Zeiger ist; dann wird mit int kombiniert, was anzeigt, dass das Element, auf das dieser Zeiger zeigt, Integer-Daten sind. Da Zeiger zweiter Stufe und Zeiger höherer Stufen in komplexen Typen selten verwendet werden, werden wir später bei den komplexeren Typen keine mehrstufigen Zeiger betrachten; höchstens werden wir nur einstufige Zeiger betrachten.
- int p(int);-- Ausgehend von p wird zuerst mit () kombiniert, was bedeutet, dass p eine Funktion ist; dann wird das Innere von () analysiert, was bedeutet, dass die Funktion einen Parameter vom Typ Integer-Variable hat; dann wird mit dem äußeren int kombiniert, was bedeutet, dass der Rückgabewert der Funktion ganzzahlige Daten sind.
- int (*p)(int);-- Ausgehend von p wird zuerst mit dem Zeigeroperator `*` kombiniert, was bedeutet, dass p ein Zeiger ist; dann wird mit () kombiniert, was bedeutet, dass der Zeiger auf eine Funktion zeigt; dann wird mit dem int innerhalb von () kombiniert, was bedeutet, dass die Funktion einen int-Parameter hat; dann wird mit dem äußersten int kombiniert, was bedeutet, dass der Rückgabetyp der Funktion Integer ist. p ist also ein Zeiger auf eine Funktion, die einen ganzzahligen Parameter hat und deren Rückgabetyp Integer ist.
- int *(*p(int))[3];-- Diesen Typ kannst du vorerst überspringen; er ist zu komplex. Ausgehend von p wird zuerst mit () kombiniert, was bedeutet, dass p eine Funktion ist; dann geht man in das Innere von (), kombiniert mit int, was bedeutet, dass die Funktion einen ganzzahligen Variablenparameter hat; dann wird mit dem äußeren * kombiniert, was bedeutet, dass die Funktion einen Zeiger zurückgibt; dann geht man zur äußersten Ebene, kombiniert zuerst mit [], was bedeutet, dass der zurückgegebene Zeiger auf ein Array zeigt; dann wird mit * kombiniert, was bedeutet, dass die Elemente im Array Zeiger sind; dann wird mit int kombiniert, was bedeutet, dass der Inhalt, auf den die Zeiger zeigen, ganzzahlige Daten sind. p ist also eine Funktion, deren Parameter eine Ganzzahl ist und die eine Zeigervariable zurückgibt, die auf ein Array zeigt, das aus ganzzahligen Zeigervariablen besteht.
Das ist so ziemlich alles, was es zu sagen gibt; unsere Aufgabe ist nur so viel. Wenn du diese Typen verstehst, sind andere Typen für uns ein Kinderspiel. Allerdings verwenden wir im Allgemeinen keine übermäßig komplexen Typen, da dies die Lesbarkeit des Programms stark verringern würde. Bitte verwende sie mit Vorsicht. Die oben genannten wenigen Typen reichen für uns bereits aus.
1. Ausführliche Erklärung von Zeigern
Ein Zeiger ist eine spezielle Variable, deren gespeicherter Wert als eine Adresse im Speicher interpretiert wird. Um einen Zeiger zu verstehen, müssen Sie vier Aspekte verstehen: den Typ des Zeigers, den Typ, auf den der Zeiger zeigt, den Wert des Zeigers (oder den Speicherbereich, auf den der Zeiger zeigt), und den Speicherbereich, den der Zeiger selbst belegt. Lassen Sie uns jeden einzeln erklären.
Zuerst deklarieren wir einige Zeiger als Beispiele:
Beispiel:
1. Der Typ eines Zeigers
Aus syntaktischer Sicht müssen Sie nur den Zeigernamen aus der Zeigerdeklaration entfernen; der verbleibende Teil ist der Typ des Zeigers. Dies ist der Typ, den der Zeiger selbst hat. Sehen wir uns die Typen der verschiedenen Zeiger in Beispiel 1 an:
- 1, int *ptr;: Der Typ des Zeigers istint*
- 2, char *ptr;: Der Typ des Zeigers istchar*
- 3, int **ptr;: Der Typ des Zeigers istint**
- 4, int (*ptr)[3];: Der Typ des Zeigers istint(*)[3]
- 5, int *(*ptr)[4]; : Der Typ des Zeigers istint*(*)[4]
Na, wie ist das? Die Methode, den Typ eines Zeigers zu ermitteln, ist sehr einfach, nicht wahr?
2. Der Typ, auf den der Zeiger zeigt
Wenn Sie einen Zeiger verwenden, um auf den Speicherbereich zuzugreifen, auf den der Zeiger zeigt, bestimmt der Typ, auf den der Zeiger zeigt, als was der Compiler den Inhalt dieses Speicherbereichs behandelt.
Syntaktisch müssen Sie nur den Zeigernamen und den links davon stehenden Zeiger-Deklarationsoperator `*` aus der Zeigerdeklarationsanweisung entfernen; was übrig bleibt, ist der Typ, auf den der Zeiger zeigt. Zum Beispiel:
- 1, int*ptr;: Der Typ, auf den der Zeiger zeigt, istint
- 2, char*ptr;: Der Typ, auf den der Zeiger zeigt, istchar
- 3, int**ptr;: Der Typ, auf den der Zeiger zeigt, istint*
- 4, int(*ptr)[3];: Der Typ, auf den der Zeiger zeigt, istint()[3]
- 5, int*(*ptr)[4]; : Der Typ, auf den der Zeiger zeigt, istint*()[4]
In der Zeigerarithmetik spielt der Typ, auf den der Zeiger zeigt, eine bedeutende Rolle.
Der Typ des Zeigers (d. h. der Typ des Zeigers selbst) und der Typ, auf den der Zeiger zeigt, sind zwei Konzepte. Je vertrauter Sie mit C werden, werden Sie feststellen, dass die Aufteilung des mit Zeigern verbundenen Konzepts des "Typs" in die beiden Konzepte "Typ des Zeigers" und "Typ, auf den der Zeiger zeigt" einer der Schlüsselpunkte zum Beherrschen von Zeigern ist. Ich habe viele Bücher gelesen und festgestellt, dass in einigen schlecht geschriebenen Büchern diese beiden Konzepte vermischt werden, sodass die Bücher widersprüchlich erscheinen und man, je mehr man liest, desto verwirrter wird.
3. Der Wert eines Zeigers — Oder der Speicherbereich bzw. die Adresse, auf den bzw. die der Zeiger zeigt
Der Wert eines Zeigers ist der numerische Wert, der im Zeiger selbst gespeichert ist. Dieser Wert wird vom Compiler als Adresse behandelt, nicht als gewöhnlicher numerischer Wert. In 32-Bit-Programmen ist der Wert eines Zeigers beliebigen Typs eine 32-Bit-Ganzzahl, weil in 32-Bit-Programmen Speicheradressen alle 32 Bit lang sind. Der Speicherbereich, auf den ein Zeiger zeigt, beginnt an der Speicheradresse, die durch den Wert des Zeigers dargestellt wird, und hat eine Länge von sizeof(Typ, auf den der Zeiger zeigt). Ab jetzt, wenn wir sagen, der Wert eines Zeigers sei XX, ist das gleichbedeutend damit, dass der Zeiger auf einen Speicherbereich zeigt, dessen Startadresse XX ist; wenn wir sagen, ein Zeiger zeigt auf einen bestimmten Speicherbereich, ist das gleichbedeutend damit, dass der Wert des Zeigers die Startadresse dieses Speicherbereichs ist. Der Speicherbereich, auf den ein Zeiger zeigt, und der Typ, auf den der Zeiger zeigt, sind zwei völlig unterschiedliche Konzepte. In Beispiel 1 existiert der Typ, auf den der Zeiger zeigt, bereits, aber weil der Zeiger nicht initialisiert wurde, existiert der Speicherbereich, auf den er zeigt, nicht oder ist bedeutungslos.
Von nun an solltest du jedes Mal, wenn dir ein Zeiger begegnet, fragen: Welchen Typ hat dieser Zeiger? Welchen Typ hat das, worauf er zeigt? Worauf zeigt der Zeiger? (Besonders beachten)
4. Der vom Zeiger selbst belegte Speicherbereich
Wie viel Speicher belegt der Zeiger selbst? Du musst nur die Funktion sizeof(Zeigertyp) verwenden, um ihn zu messen. Auf einer 32-Bit-Plattform belegt ein Zeiger selbst 4 Bytes. Das Konzept des vom Zeiger selbst belegten Speichers ist nützlich, um zu bestimmen, ob ein Zeigerausdruck (später erklärt) ein lvalue ist.
2. Arithmetische Operationen mit Zeigern
Zu einem Zeiger kann eine Ganzzahl addiert oder von ihm subtrahiert werden. Die Bedeutung dieser Operation für Zeiger unterscheidet sich von der gewöhnlichen numerischen Addition und Subtraktion; sie erfolgt in Einheiten von Elementen. Zum Beispiel:
Im obigen Beispiel ist der Typ des Zeigers ptr int*, und der Typ, auf den er zeigt, ist int. Er wird initialisiert, um auf die Ganzzahlvariable a zu zeigen. In der nächsten Anweisung wird der Zeiger ptr um 1 inkrementiert. Der Compiler behandelt dies wie folgt: Er addiert sizeof(int) zum Wert des Zeigers ptr. In einem 32-Bit-Programm addiert dies 4, weil int in einem 32-Bit-Programm 4 Bytes belegt. Da Adressen in Bytes gemessen werden, hat sich die Adresse, auf die ptr zeigt, um 4 Bytes von der Adresse der ursprünglichen Variablen a in Richtung höherer Adressen erhöht. Da die Länge eines char-Typs ein Byte beträgt, zeigte ptr ursprünglich auf die vier Bytes beginnend bei Element 0 des Arrays a; an diesem Punkt zeigt er auf die vier Bytes beginnend bei Element 4 des Arrays a. Wir können einen Zeiger und eine Schleife verwenden, um ein Array zu durchlaufen. Sehen Sie sich das Beispiel an:
Dieses Beispiel inkrementiert den Wert jedes Elements im Integer-Array um 1. Da jede Schleife den Zeiger ptr um ein Element inkrementiert, kann jede Schleife auf das nächste Element des Arrays zugreifen.
Sehen Sie sich ein weiteres Beispiel an:
In diesem Beispiel wird ptr um 5 inkrementiert. Der Compiler behandelt dies so: Er addiert 5-mal sizeof(int) zum Wert des Zeigers ptr; in einem 32-Bit-Programm bedeutet das, 5-mal 4 = 20 zu addieren. Da die Einheit einer Adresse ein Byte ist, hat sich die Adresse, auf die ptr jetzt zeigt, um 20 Bytes in Richtung höherer Adressen im Vergleich zu der Adresse verschoben, auf die vor dem Addieren von 5 gezeigt wurde.
In diesem Beispiel zeigt ptr vor dem Hinzufügen von 5 auf die vier Bytes, die bei Element 0 des Arrays a beginnen. Nach dem Hinzufügen von 5 zeigt ptr über den gültigen Bereich des Arrays a hinaus. Obwohl dies in der Praxis Probleme verursachen kann, ist es syntaktisch erlaubt. Dies spiegelt auch die Flexibilität von Zeigern wider. Wenn im obigen Beispiel ptr um 5 verringert würde, wäre der Vorgang weitgehend derselbe, außer dass der Wert von ptr um das 5-Fache von sizeof(int) verringert würde und die Adresse, auf die der neue ptr zeigt, sich gegenüber der ursprünglich von ptr gezeigten Adresse um 20 Bytes in Richtung niedrigerer Adressen verschieben würde.
Nun möchte ich ein weiteres Beispiel geben: (ein Missverständnis)
Missverständnis 1. Die Ausgabe ist Y und o.
Missverständnis: ptr ist ein Zeiger zweiter Stufe auf char. Wenn ptr++ ausgeführt wird, wird der Zeiger um sizeof(char) erhöht, daher ist die Ausgabe wie oben. Dies dürfte nur bei wenigen Personen das Ergebnis sein.Missverständnis 2.Die Ausgabe ist Y und a. Missverständnis: ptr zeigt auf einen Typ char *. Wenn ptr++ ausgeführt wird, wird der Zeiger um sizeof(char *) erhöht (manche denken vielleicht, dieser Wert sei 1, was zur Antwort in Missverständnis 1 führen würde; dieser Wert sollte 4 sein, siehe vorheriger Inhalt), das heißt &p+4. Würde dann eine Dereferenzierung nicht auf das fünfte Element im Array zeigen? Wäre die Ausgabe dann nicht das fünfte Element im Array? Die Antwort ist nein.
Korrekte Lösung:Der Typ von ptr ist char **, und der Typ, auf den er zeigt, ist char *. Die Adresse, auf die er zeigt, ist die Adresse von p (&p). Wenn ptr++ ausgeführt wird, wird der Zeiger um sizeof(char*) erhöht, das heißt &p+4. Worauf zeigt dann *(&p+4)? Das kannst du Gott fragen; vielleicht wird Er dir sagen, wohin. Die endgültige Ausgabe wird also ein zufälliger Wert sein, oder vielleicht eine illegale Operation.
. Aber wenn array als Zeiger betrachtet wird, zeigt es auf das 0. Element des Arrays. Sein Typ ist int*, und der Typ, auf den es zeigt, ist der Typ des Array-Elements, nämlich int. Daher ist es überhaupt nicht überraschend, dass *array gleich 0 ist. Ähnlich ist array+3 ein Zeiger auf das 3. Element des Arrays, also
Nachdem eine Ganzzahl n zu einem Zeiger ptrold addiert (oder von ihm subtrahiert) wurde, ist das Ergebnis ein neuer Zeiger ptrnew. Der Typ von ptrnew ist derselbe wie der Typ von ptrold, und der Typ, auf den ptrnew zeigt, ist ebenfalls derselbe wie der Typ, auf den ptrold zeigt. Der Wert von ptrnew wird im Vergleich zum Wert von ptrold um n mal sizeof(Typ, auf den ptrold zeigt) Bytes erhöht (oder verringert). Das heißt, der Speicherbereich, auf den ptrnew zeigt, hat sich im Vergleich zum Speicherbereich, auf den ptrold zeigt, um n mal sizeof(Typ, auf den ptrold zeigt) Bytes in Richtung höherer (oder niedrigerer) Adressen verschoben. Addition und Subtraktion von Zeigern: Zwei Zeiger können nicht addiert werden; dies ist eine ungültige Operation, denn nach der Addition zeigt das Ergebnis auf eine Stelle unbekannter Richtung und ist bedeutungslos. Zwei Zeiger können subtrahiert werden, aber sie müssen denselben Typ haben. Dies wird im Allgemeinen bei Arrays verwendet; ich werde nicht näher darauf eingehen.
3. Die Operatoren & und *
Hier&ist der Adressoperator,*ist der Indirektionsoperator.
&aDas Ergebnis der Operation ist ein Zeiger. Der Typ des Zeigers ist der Typ von a plus ein *, der Typ, auf den der Zeiger zeigt, ist der Typ von a, und die Adresse, auf die der Zeiger zeigt, ist, nun ja, die Adresse von a.
*pDas Ergebnis der Operation ist allerlei Dinge. Kurz gesagt, das Ergebnis von *p ist das Ding, auf das p zeigt. Dieses Ding hat folgende Eigenschaften: sein Typ ist der Typ, auf den p zeigt, und die Adresse, die es belegt, ist die Adresse, auf die p zeigt.
Beispiel:
4. Zeigerausdrücke
Wenn das Ergebnis eines Ausdrucks ein Zeiger ist, dann wird dieser Ausdruck als Zeigerausdruck bezeichnet.
Das Folgende sind einige Beispiele für Zeigerausdrücke:
Beispiel:
Da das Ergebnis eines Zeigerausdrucks ein Zeiger ist, hat ein Zeigerausdruck ebenfalls die vier Elemente eines Zeigers: den Typ des Zeigers, den Typ, auf den der Zeiger zeigt, den Speicherbereich, auf den der Zeiger zeigt, und den Speicher, den der Zeiger selbst belegt.
Also gut: Wenn der resultierende Zeiger eines Zeigerausdrucks bereits explizit Speicher besitzt, der vom Zeiger selbst belegt wird, dann ist dieser Zeigerausdruck ein Lvalue; andernfalls ist er kein Lvalue. In Beispiel 7 ist &a kein Lvalue, weil es noch keinen expliziten Speicher belegt hat. *ptr ist ein Lvalue, weil der Zeiger *ptr bereits Speicher belegt hat. Tatsächlich ist *ptr der Zeiger pa. Da pa bereits seinen eigenen Platz im Speicher hat, hat *ptr natürlich auch seinen eigenen Platz.
5. Die Beziehung zwischen Arrays und Zeigern
Der Array-Name eines Arrays kann tatsächlich als Zeiger betrachtet werden. Betrachten Sie das folgende Beispiel:
Im obigen Beispiel repräsentiert der Arrayname array im Allgemeinen das Array selbst, mit dem Typ int*(array+3)ergibt 3. Der Rest lässt sich analog ableiten.
Beispiel:
Im obigen Beispiel ist str ein Array aus drei Elementen, und jedes Element des Arrays ist ein Zeiger, wobei jeder Zeiger auf eine Zeichenkette zeigt. Wird der Zeiger-Array-Name str als Zeiger behandelt, zeigt er auf das 0. Element des Arrays. Sein Typ ist char **, und der Typ, auf den er zeigt, ist char *.
*str ist ebenfalls ein Zeiger. Sein Typ ist char *, und der Typ, auf den er zeigt, ist char. Die Adresse, auf die er zeigt, ist die Adresse des ersten Zeichens der Zeichenkette "Hello,thisisasample!", d. h. die Adresse von 'H'. Hinweis: Eine Zeichenkette ist äquivalent zu einem Array; sie wird im Speicher in Form eines Arrays abgelegt. Allerdings ist eine Zeichenkette eine Array-Konstante, ihr Inhalt kann nicht geändert werden, und sie kann nur ein Rvalue sein. Als Zeiger betrachtet ist sie sowohl ein Zeiger auf eine Konstante als auch ein konstanter Zeiger.
str+1 ist ebenfalls ein Zeiger. Er zeigt auf das 1. Element des Arrays. Sein Typ ist char**, und der Typ, auf den er zeigt, ist char*.
*(str+1) ist ebenfalls ein Zeiger. Sein Typ ist char*, und der Typ, auf den er zeigt, ist char. Er zeigt auf das erste Zeichen 'H' von "Hi,goodmorning.".
Nun möchte ich das Problem des Array-Namens eines Arrays zusammenfassen (wobei das, was im Array gespeichert ist, ebenfalls ein Array ist):
Ein Array wird deklariertTYPE array[n], dann hat der Array-Name array zwei Bedeutungen:
-
Erstens repräsentiert es das gesamte Array; sein Typ istTYPE[n];
-
Zweitens ist es ein konstanter Zeiger; der Typ dieses Zeigers istTYPE*, und der Typ, auf den dieser Zeiger zeigt, istTYPE, das heißt der Typ des Array-Elements. Der Speicherbereich, auf den dieser Zeiger zeigt, ist das 0-te Element des Arrays. Der Zeiger selbst belegt einen separaten Speicherbereich; beachten Sie, dass er sich von dem Speicherbereich unterscheidet, der vom 0-ten Element des Arrays belegt wird. Der Wert dieses Zeigers kann nicht geändert werden, das heißt, Ausdrücke wie array++ sind falsch. In verschiedenen Ausdrücken kann der Array-Name array unterschiedliche Rollen spielen. Im Ausdruck sizeof(array) repräsentiert der Array-Name array das Array selbst, daher misst die Funktion sizeof zu diesem Zeitpunkt die Größe des gesamten Arrays.
Im Ausdruck *array spielt array die Rolle eines Zeigers, daher ist das Ergebnis dieses Ausdrucks der Wert des 0-ten Elements des Arrays.sizeof(*array)Gemessen wird die Größe eines Array-Elements.
Im Ausdruck array+n (wobei n=0, 1, 2, ...), fungiert array als Zeiger, daher ist das Ergebnis von array+n ein Zeiger, dessen TypTYPE *ist, der Typ, auf den er zeigt, ist TYPE, und er zeigt auf das n-te Element des Arrays. Daher misst sizeof(array+n) die Größe des Zeigertyps. In einem 32-Bit-Programm ist das Ergebnis 4.
Beispiel:
Im vorherigen Beispiel ist ptr ein Zeiger, dessen Typint(*)[10]ist, der Typ, auf den er zeigt, ist int
In diesem Abschnitt wurde die Funktion sizeof() erwähnt. Nun möchte ich fragen: Misst sizeof(Zeigername) die Größe des Typs des Zeigers selbst oder die Größe des Typs, auf den er zeigt?
Die Antwort ist die erstere. Zum Beispiel:
int(*ptr)[10];
Dann haben wir in einem 32-Bit-Programm:
sizeof(int(*)[10])==4 sizeof(int[10])==40 sizeof(ptr)==4
Tatsächlich misst sizeof(object) immer die Größe des eigenen Typs des Objekts, nicht die Größe eines anderen Typs.
6. Die Beziehung zwischen Zeigern und Strukturtypen
Sie können einen Zeiger auf ein Objekt eines Strukturtyps deklarieren. Beispiel:
Wie greifen Sie über den Zeiger ptr auf die drei Mitgliedsvariablen von ss zu?
Antwort:
ptr->a; //指向运算符,或者可以这们(*ptr).a,建议使用前者 ptr->b; ptr->c;
Und wie greifen Sie über den Zeiger pstr auf die drei Mitgliedsvariablen von ss zu?
Antwort:
*pstr; //访问了ss 的成员a. *(pstr+1); //访问了ss 的成员b. *(pstr+2) //访问了ss 的成员c.
Obwohl ich den obigen Code auf meinem MSVC++6.0 debuggt habe, sollten Sie wissen, dass die Verwendung von pstr auf diese Weise für den Zugriff auf Strukturmitglieder nicht standardkonform ist. Um zu erklären, warum es nicht standardkonform ist, sehen wir uns an, wie man über einen Zeiger auf jedes Element eines Arrays zugreift: (ersetzen Sie die Struktur durch ein Array)
In der Form sieht es genau wie die nicht standardmäßige Methode aus, über einen Zeiger auf Strukturmitglieder zuzugreifen.
Alle C/C++-Compiler speichern beim Anordnen von Array-Elementen die einzelnen Array-Elemente immer in zusammenhängendem Speicher, ohne Lücken zwischen den Elementen. Beim Speichern der Mitglieder eines Strukturobjekts kann jedoch in manchen Kompilierungsumgebungen Wortausrichtung, Doppelwortausrichtung oder eine andere Ausrichtung erforderlich sein, und zwischen benachbarten Mitgliedern müssen möglicherweise mehrere "Padding-Bytes" eingefügt werden. Dadurch können zwischen den Mitgliedern Lücken von mehreren Bytes entstehen.
Daher gibt es in Beispiel 12 selbst dann, wenn *pstr auf die erste Mitgliedsvariable a des Strukturobjekts ss zugreift, keine Garantie dafür, dass *(pstr+1) unbedingt auf das Strukturmitglied b zugreift. Denn zwischen Mitglied a und Mitglied b können sich mehrere Padding-Bytes befinden, und vielleicht greift *(pstr+1) genau auf diese Padding-Bytes zu. Das beweist auch die Flexibilität von Zeigern. Wenn es Ihr Ziel ist, zu sehen, ob es Padding-Bytes zwischen Strukturmitgliedern gibt, hey, dann ist das tatsächlich eine gute Methode.
Die korrekte Methode für einen Zeiger, auf Strukturmitglieder zuzugreifen, sollte jedoch die Methode unter Verwendung des Zeigers ptr wie in Beispiel 12 sein.
7. Die Beziehung zwischen Zeigern und Funktionen
Sie können einen Zeiger als Zeiger auf eine Funktion deklarieren.
int fun1(char *,int);
int (*pfun1)(char *,int);
pfun1=fun1;
int a=(*pfun1)("abcdefg",7); //通过函数指针调用函数。
Sie können einen Zeiger als formalen Parameter einer Funktion verwenden. In einer Funktionsaufrufanweisung kann ein Zeigerausdruck als tatsächliches Argument verwendet werden.
Die Funktion fun in diesem Beispiel berechnet die Summe der ASCII-Code-Werte jedes Zeichens in einer Zeichenkette. Wie bereits erwähnt, ist ein Array-Name auch ein Zeiger. Im Funktionsaufruf, wenn str als tatsächliches Argument an den formalen Parameter s übergeben wird, wird tatsächlich der Wert von str an s übergeben. Die Adresse, auf die s zeigt, wird dieselbe wie die Adresse, auf die str zeigt, aber str und s belegen jeweils ihren eigenen Speicherplatz. Ein Inkrementieren von s um 1 innerhalb des Funktionskörpers bedeutet nicht, dass str gleichzeitig ebenfalls um 1 inkrementiert wird.
8. Zeigertypumwandlung
Wenn wir einen Zeiger initialisieren oder einem Zeiger einen Wert zuweisen, ist die linke Seite des Zuweisungsoperators ein Zeiger und die rechte Seite des Zuweisungsoperators ein Zeigerausdruck. In den meisten der zuvor gegebenen Beispiele ist der Typ des Zeigers derselbe wie der Typ des Zeigerausdrucks, und der Typ, auf den der Zeiger zeigt, ist derselbe wie der Typ, auf den der Zeigerausdruck zeigt.
Was sollten wir im obigen Beispiel tun, wenn wir möchten, dass der Zeiger p auf die reelle Zahl f zeigt?
Ist es die folgende Anweisung?
p=&f;
Nein. Denn der Typ des Zeigers p ist int *, und der Typ, auf den er zeigt, ist int. Das Ergebnis des Ausdrucks &f ist ein Zeiger; der Typ des Zeigers ist float *, und der Typ, auf den er zeigt, ist float.
Die beiden sind nicht konsistent, daher funktioniert eine direkte Zuweisung nicht. Zumindest bei meinem MSVC++6.0 erfordert eine Zeigerzuweisungsanweisung, dass die Typen auf beiden Seiten des Zuweisungsoperators gleich sind und auch die Typen, auf die gezeigt wird, gleich sind. Ich habe es nicht mit anderen Compilern versucht; du kannst es ausprobieren. Um unser Ziel zu erreichen, müssen wir eine „erzwungene Typumwandlung“ durchführen:
p=(int*)&f;
Wenn es einen Zeiger p gibt und wir seinen Typ und den Typ, auf den er zeigt, inTYEP *TYPEändern müssen, dann lautet die Syntax:(TYPE *)p
Das Ergebnis einer solchen erzwungenen Typkonvertierung ist ein neuer Zeiger. Der Typ des neuen Zeigers istTYPE *, und der Typ, auf den er zeigt, istTYPE, und die Adresse, auf die er zeigt, ist die Adresse, auf die der ursprüngliche Zeiger zeigt. Keine der Eigenschaften des ursprünglichen Zeigers p wird geändert. (Merken Sie sich das)
Wenn eine Funktion Zeiger als formale Parameter verwendet, dann müssen während der Bindung von tatsächlichen Argumenten und formalen Parametern in einer Funktionsaufrufanweisung die Typen übereinstimmen; andernfalls ist eine erzwungene Konvertierung erforderlich:
Beachten Sie, dass dies ein 32-Bit-Programm ist, also belegt der Typ int vier Bytes und der Typ char ein Byte. Die Funktion fun kehrt die Reihenfolge der vier Bytes einer Ganzzahl um. Ist Ihnen das aufgefallen? In der Funktionsaufrufanweisung ist das Ergebnis des tatsächlichen Arguments &a ein Zeiger; sein Typ ist int *, und der Typ, auf den er zeigt, ist int. Der Typ des formalen Parameterzeigers ist char *, und der Typ, auf den er zeigt, ist char. Daher müssen wir während der Bindung des tatsächlichen Arguments und des formalen Parameters eine Konvertierung von int * nach char * durchführen.
Im Zusammenhang mit diesem Beispiel können wir Folgendes tun:
Stellen Sie sich den Konvertierungsprozess des Compilers vor: der Compiler erstellt zuerst einen temporären Zeiger char *temp, führt dann temp=(char *)&a aus und übergibt schließlich den Wert von temp an s. Das Endergebnis ist also: Der Typ von s ist char *, der Typ, auf den er zeigt, ist char, und die Adresse, auf die er zeigt, ist die Startadresse von a.
Wir wissen bereits, dass der Wert eines Zeigers die Adresse ist, auf die er zeigt. In einem 32-Bit-Programm ist der Wert eines Zeigers tatsächlich eine 32-Bit-Ganzzahl.
Können wir eine Ganzzahl direkt als Wert eines Zeigers einem Zeiger zuweisen? Wie in den folgenden Anweisungen:
Streng genommen ist das (TYPE *) hier nicht dasselbe wie das (TYPE *) bei der Zeigertypumwandlung. Die Bedeutung von (TYPE*) hier ist, den Wert des unsigned integer a als Adresse zu behandeln. Oben wurde betont, dass der Wert von a eine gültige Adresse darstellen muss; andernfalls tritt, wenn du ptr verwendest, ein Fehler wegen einer unzulässigen Operation auf. Überlege, ob das Umgekehrte möglich ist: die Adresse, auf die ein Zeiger zeigt, d. h. den Wert des Zeigers, als Integer zu extrahieren. Auf jeden Fall. Das folgende Beispiel zeigt, wie man den Wert eines Zeigers als Integer extrahiert und dann diesen Integer als Adresse einem Zeiger zuweist:
Jetzt wissen wir bereits, dass wir den Wert eines Zeigers als Integer extrahieren können und dass wir auch einen Integer-Wert einem Zeiger als Adresse zuweisen können.
9. Sicherheitsprobleme bei Zeigern
Betrachten Sie das folgende Beispiel:
Der Zeiger ptr ist ein Zeiger vom Typ int *, und der Typ, auf den er zeigt, ist int. Die Adresse, auf die er zeigt, ist die Startadresse von s. In einem 32-Bit-Programm belegt s ein Byte, und der Typ int belegt vier Bytes. Die letzte Anweisung ändert nicht nur das eine Byte, das von s belegt wird, sondern ändert auch die drei Bytes, die in Richtung höherer Adressen an s angrenzen. Wofür werden diese drei Bytes verwendet? Nur der Compiler weiß es, und der Programmierer wird es wahrscheinlich nicht wissen. Vielleicht speichern diese drei Bytes sehr wichtige Daten, oder vielleicht sind diese drei Bytes zufällig ein Stück Programmcode. Wegen Ihrer unvorsichtigen Verwendung des Zeigers wurden die Werte dieser drei Bytes geändert! Dies wird einen fatalen Fehler verursachen.
Betrachten wir ein weiteres Beispiel:
Dieses Beispiel kann sicherlich kompiliert und ausgeführt werden. Aber sehen Sie? Nachdem Anweisung 3 den Zeiger ptr um 1 inkrementiert hat, zeigt ptr auf einen Speicherbereich in Richtung höherer Adressen, der an die Integer-Variable a angrenzt. Was befindet sich in diesem Speicherbereich? Wir wissen es nicht. Es könnten sehr wichtige Daten sein oder sogar ein Stück Code.
Doch Anweisung 4 schreibt tatsächlich Daten in diesen Speicherbereich! Das ist ein schwerwiegender Fehler. Daher muss der Programmierer bei der Verwendung von Zeigern ganz klar im Kopf haben: Worauf genau zeigt mein Zeiger? Wenn Zeiger für den Zugriff auf Arrays verwendet werden, muss ebenfalls darauf geachtet werden, die untere oder obere Grenze des Arrays nicht zu überschreiten, da dies ebenfalls ähnliche Fehler verursachen kann.
Bei der erzwungenen Typumwandlung von Zeigern:ptr1=(TYPE *)ptr2Wenn sizeof(Typ von ptr2) größer als sizeof(Typ von ptr1) ist, dann ist es sicher, den Zeiger ptr1 für den Zugriff auf den Speicherbereich zu verwenden, auf den ptr2 zeigt. Wenn sizeof(Typ von ptr2) kleiner als sizeof(Typ von ptr1) ist, dann ist es unsicher, den Zeiger ptr1 für den Zugriff auf den Speicherbereich zu verwenden, auf den ptr2 zeigt. Warum das so ist, sollte der Leser verstehen können, wenn er darüber in Verbindung mit Beispiel 18 nachdenkt.