Der Zweck eines Zeigers besteht darin, die Adresse einer bestimmten Variablen zu speichern. Dann sollte die Speicherstruktur des folgenden Codes folgendermaßen aussehen:
int a = 5;
int *b = &a;
...... Speicheradresse ...... Wert
a ... 0x000002 ................... 5
b ... 0x000010 ..... .............. 0x000002
Okay gut. Dann nehme ich an, dass ich jetzt die Adresse des Zeigers * b speichern möchte. Dann definieren wir im Allgemeinen einen Doppelzeiger ** c als
int a = 5;
int *b = &a;
int **c = &b;
Dann sieht die Speicherstruktur so aus:
...... Speicheradresse ...... Wert
a ... 0x000002 ................... 5
b ... 0x000010 ..... .............. 0x000002
c ... 0x000020 ................... 0x000010
Also bezieht sich ** c auf die Adresse von * b.
Meine Frage ist nun, warum diese Art von Code funktioniert.
int a = 5;
int *b = &a;
int *c = &b;
eine Warnung generieren?
Wenn der Zweck des Zeigers nur darin besteht, die Speicheradresse zu speichern, sollte es meiner Meinung nach keine Hierarchie geben, wenn sich die zu speichernde Adresse auf eine Variable, einen Zeiger, einen Doppelzeiger usw. bezieht, daher sollte der folgende Codetyp verwendet werden gültig sein.
int a = 5;
int *b = &a;
int *c = &b;
int *d = &c;
int *e = &d;
int *f = &e;
warning: incompatible pointer types initializing 'int *' with an expression of type 'int **'; remove & [-Wincompatible-pointer-types]
. Dies könnte alles klar gemacht haben.Antworten:
Im
int a = 5; int *b = &a; int *c = &b;
Sie erhalten eine Warnung, weil sie
&b
vom Typ istint **
, und Sie versuchen, eine Variable vom Typ zu initialisierenint *
. Es gibt keine impliziten Konvertierungen zwischen diesen beiden Typen, die zur Warnung führen.Um das längere Beispiel zu nehmen, an dem Sie arbeiten möchten: Wenn wir versuchen, dereferenzieren
f
, gibt uns der Compilerint
einen Zeiger, den wir weiter dereferenzieren können.Beachten Sie auch, dass auf vielen Systemen
int
undint*
nicht dieselbe Größe vorhanden ist (z. B. kann ein Zeiger 64 Bit lang undint
32 Bit lang sein). Wenn Sie dereferenzierenf
und eine erhaltenint
, verlieren Sie die Hälfte des Werts und können ihn dann nicht einmal in einen gültigen Zeiger umwandeln.quelle
void *
gab es nicht einmal, man konnte es immer nur benutzenchar *
:)Zur Laufzeit enthält ein Zeiger nur eine Adresse. Zur Kompilierungszeit ist jeder Variablen jedoch auch ein Typ zugeordnet. Wie die anderen gesagt haben
int*
undint**
zwei verschiedene, inkompatible Typen sind.Es gibt einen Typ,
void*
der macht, was Sie wollen: Er speichert nur eine Adresse, Sie können ihm eine beliebige Adresse zuweisen:int a = 5; int *b = &a; void *c = &b;
Wenn Sie jedoch eine Dereferenzierung vornehmen möchten
void*
, müssen Sie die 'fehlenden' Typinformationen selbst angeben:int a2 = **((int**)c);
quelle
int
kann auch eine andere Größe haben, hat aberint32_t
garantiert genau 32 Bit.Sie müssen zu den Grundlagen zurückkehren.
p
es sich um einen Zeigerwert handelt,*p
handelt es sich um eine Variablev
es sich um eine Variable&v
handelt, handelt es sich um einen ZeigerUnd jetzt können wir alle Fehler in Ihrem Beitrag finden.
Nr.
*b
Ist eine Variable vom Typ int. Es ist kein Zeiger.b
ist eine Variable, deren Wert ein Zeiger ist.*b
ist eine Variable, deren Wert eine Ganzzahl ist.NEIN NEIN NEIN. Absolut nicht. Sie müssen dies richtig verstehen, wenn Sie Zeiger verstehen wollen.
*b
ist eine Variable; Es ist ein Alias für die Variablea
. Die Adresse der Variablena
ist der Wert der Variablenb
.**c
bezieht sich nicht auf die Adresse vona
. Es ist vielmehr eine Variable , die ein Alias für eine Variable ista
. (Und so ist es auch*b
.)Die richtige Aussage lautet: Der Wert der Variablen
c
ist die Adresse vonb
. Oder äquivalent: Der Wert vonc
ist ein Zeiger, auf den verwiesen wirdb
.Woher wissen wir das? Gehen Sie zurück zu den Grundlagen. Das hast du gesagt
c = &b
. Was ist der Wert vonc
? Ein Zeiger. Zu was?b
.Stellen Sie sicher, dass Sie die grundlegenden Regeln vollständig verstehen.
Nachdem Sie hoffentlich die richtige Beziehung zwischen Variablen und Zeigern verstanden haben, sollten Sie in der Lage sein, Ihre Frage zu beantworten, warum Ihr Code einen Fehler ausgibt.
quelle
int * b;
wir sind nicht nur sagen "b
ist eine Variable vom Typint*
. Wir sagen auch , dass*b
eine Variable vom Typ istint
die hier geniale Idee ist , dass Sie geistig als beide betrachten können.int* b
Undint *b
und entweder Interpretation korrekt ist .Das Typsystem von C erfordert dies, wenn Sie eine korrekte Warnung erhalten möchten und wenn der Code überhaupt kompiliert werden soll. Mit nur einer Ebene der Zeigertiefe würden Sie nicht wissen, ob der Zeiger auf einen Zeiger oder auf eine tatsächliche Ganzzahl zeigt.
Wenn Sie einen Typ dereferenzieren
int**
, wissen Sie, dass der Typ, den Sie erhalten, ist,int*
und wenn Sie deneferenzieren, istint*
der Typ ähnlichint
. Bei Ihrem Vorschlag wäre der Typ nicht eindeutig.Anhand Ihres Beispiels ist es unmöglich zu wissen, ob
c
Punkte auf aint
oderint*
:c = rand() % 2 == 0 ? &a : &b;
Auf welchen Typ zeigt c? Der Compiler weiß das nicht, daher ist es unmöglich, diese nächste Zeile auszuführen:
In C gehen alle Typinformationen nach dem Kompilieren verloren, da jeder Typ zur Kompilierungszeit überprüft wird und nicht mehr benötigt wird. Ihr Vorschlag würde tatsächlich Speicher und Zeit verschwenden, da jeder Zeiger zusätzliche Laufzeitinformationen zu den in Zeigern enthaltenen Typen haben müsste.
quelle
Zeiger sind Abstraktionen von Speicheradressen mit zusätzlicher Typensemantik und in einer Sprache wie dem Typ C.
Zunächst einmal gibt es keine Garantie , dass
int *
undint **
die gleiche Größe oder Darstellung haben (auf modernen Desktop - Architekturen sie es tun, aber man kann sich nicht darauf verlassen allgemein wahr zu sein).Zweitens ist der Typ für die Zeigerarithmetik von Bedeutung. Bei einem Zeiger
p
vom Typ ergibtT *
der Ausdruckp + 1
die Adresse des nächsten Objekts vom TypT
. Nehmen Sie also die folgenden Erklärungen an:char *cp = 0x1000; short *sp = 0x1000; // assume 16-bit short int *ip = 0x1000; // assume 32-bit int long *lp = 0x1000; // assume 64-bit long
Der Ausdruck
cp + 1
gibt uns die Adresse des nächstenchar
Objekts, das wäre0x1001
. Der Ausdrucksp + 1
gibt uns die Adresse des nächstenshort
Objekts, das wäre0x1002
.ip + 1
gibt uns0x1004
undlp + 1
gibt uns0x1008
.Also gegeben
int a = 5; int *b = &a; int **c = &b;
b + 1
gibt uns die Adresse des nächstenint
undc + 1
gibt uns die Adresse des nächsten Zeigers aufint
.Zeiger auf Zeiger sind erforderlich, wenn eine Funktion in einen Parameter vom Zeigertyp schreiben soll. Nehmen Sie den folgenden Code:
void foo( T *p ) { *p = new_value(); // write new value to whatever p points to } void bar( void ) { T val; foo( &val ); // update contents of val }
Dies gilt für jeden Typ
T
. Wenn wir durchT
einen Zeigertyp ersetzenP *
, wird der Codevoid foo( P **p ) { *p = new_value(); // write new value to whatever p points to } void bar( void ) { P *val; foo( &val ); // update contents of val }
Die Semantik ist genau gleich, nur die Typen unterscheiden sich; Der formale Parameter
p
ist immer eine Indirektionsebene mehr als die Variableval
.quelle
Ohne die "Hierarchie" wäre es sehr einfach, UB ohne Warnungen überall zu generieren - das wäre schrecklich.
Bedenken Sie:
char c = 'a'; char* pc = &c; char** ppc = &pc; printf("%c\n", **ppc); // compiles ok and is valid printf("%c\n", **pc); // error: invalid type argument of unary ‘*’
Der Compiler gibt mir einen Fehler und hilft mir dadurch zu wissen, dass ich etwas falsch gemacht habe und den Fehler beheben kann.
Aber ohne "Hierarchie" wie:
char c = 'a'; char* pc = &c; char* ppc = &pc; printf("%c\n", **ppc); // compiles ok and is valid printf("%c\n", **pc); // compiles ok but is invalid
Der Compiler kann keinen Fehler geben, da es keine "Hierarchie" gibt.
Aber wenn die Linie:
printf("%c\n", **pc);
ausgeführt wird, ist es UB (undefiniertes Verhalten).
Zuerst
*pc
liest das ,char
als ob es ein Zeiger war, also liest wahrscheinlich 4 oder 8 Bytes , obwohl wir nur 1 Byte reserviert. Das ist UB.Wenn das Programm aufgrund der obigen UB nicht abstürzt, sondern nur einen Müllwert zurückgibt, besteht der zweite Schritt darin, den Müllwert zu dereferenzieren. Noch einmal UB.
Fazit
Das Typsystem hilft uns, Fehler zu erkennen, indem int *, int **, int *** usw. als verschiedene Typen angezeigt werden.
quelle
Ich denke, hier ist Ihr Missverständnis: Der Zweck des Zeigers selbst besteht darin, die Speicheradresse zu speichern, aber ein Zeiger hat normalerweise auch einen Typ, damit wir wissen, was an der Stelle zu erwarten ist, auf die er zeigt.
Insbesondere im Gegensatz zu Ihnen möchten andere Personen diese Art von Hierarchie wirklich haben, um zu wissen, was mit dem Speicherinhalt zu tun ist, auf den der Zeiger zeigt.
Es ist genau der Punkt des Zeigersystems von C, an den Typinformationen angehängt sind.
Wenn Sie tun
int a = 5;
&a
impliziert, dass das, was Sie erhalten, ein ist,int *
so dass, wenn Sie dereferenzieren, esint
wieder ein ist.Bringen Sie das auf die nächsten Ebenen,
int *b = &a; int **c = &b;
&b
ist auch ein Zeiger. Aber ohne zu wissen, was sich dahinter verbirgt. worauf es hinweist, ist nutzlos. Es ist wichtig zu wissen, dass bei der Dereferenzierung eines Zeigers der Typ des ursprünglichen Typs angezeigt wird. Dies*(&b)
ist also einint *
und**(&b)
der ursprünglicheint
Wert, mit dem wir arbeiten.Wenn Sie der Meinung sind, dass es unter Ihren Umständen keine Hierarchie von Typen geben sollte, können Sie immer damit arbeiten
void *
, obwohl die direkte Verwendbarkeit sehr eingeschränkt ist.quelle
Nun, das gilt für die Maschine (schließlich ist ungefähr alles eine Zahl). In vielen Sprachen werden Variablen jedoch eingegeben. Dies bedeutet, dass der Compiler sicherstellen kann, dass Sie sie korrekt verwenden (Typen legen Variablen einen korrekten Kontext auf).
Es ist wahr, dass ein Zeiger auf Zeiger und ein Zeiger (wahrscheinlich) dieselbe Speichermenge verwenden, um ihren Wert zu speichern (Vorsicht, dies gilt nicht für int und Zeiger auf int, die Größe einer Adresse hängt nicht mit der Größe von a zusammen Haus).
Wenn Sie also eine Adresse einer Adresse haben, die Sie unverändert und nicht als einfache Adresse verwenden sollten, denn wenn Sie als einfacher Zeiger auf den Zeiger auf den Zeiger zugreifen, können Sie eine Adresse von int so manipulieren, als wäre es ein int , was nicht ist (ersetze int ohne irgendetwas anderes und du solltest die Gefahr sehen). Sie mögen verwirrt sein, weil dies alles Zahlen sind, aber im Alltag nicht: Ich persönlich mache einen großen Unterschied bei 1 $ und 1 Hund. Hund und $ sind Typen, Sie wissen, was Sie mit ihnen machen können.
Sie können in der Montage programmieren und machen, was Sie wollen, aber Sie werden feststellen, wie gefährlich es ist, weil Sie fast das tun können, was Sie wollen, besonders seltsame Dinge. Ja, das Ändern eines Adresswerts ist gefährlich. Angenommen, Sie haben ein autonomes Auto, das etwas an eine Adresse liefern soll, die in der Entfernung angegeben ist: 1200 Speicherstraße (Adresse), und angenommen, dass Straßenhäuser durch 100 Fuß voneinander getrennt sind (1221 ist eine ungültige Adresse). Wenn Sie Adressen nach Belieben als Ganzzahl bearbeiten können, können Sie versuchen, um 1223 zu liefern und das Paket in der Mitte des Pflasters zu lassen.
Ein anderes Beispiel könnte sein, Haus, Adresse des Hauses, Eintragsnummer in einem Adressbuch dieser Adresse. Alle diese drei sind unterschiedliche Konzepte, unterschiedliche Typen ...
quelle
Es gibt verschiedene Arten. Und dafür gibt es einen guten Grund:
Haben…
int a = 5; int *b = &a; int **c = &b;
… der Ausdruck …
*b * 5
… Ist gültig, während der Ausdruck…
*c * 5
macht keinen Sinn.
Die große Sache ist nicht, wie Zeiger oder Zeiger auf Zeiger gespeichert werden, sondern worauf sie sich beziehen.
quelle
Die C-Sprache ist stark typisiert. Dies bedeutet, dass es für jede Adresse einen Typ gibt , der dem Compiler mitteilt, wie der Wert an dieser Adresse zu interpretieren ist.
In Ihrem Beispiel:
int a = 5; int *b = &a;
Der Typ von
a
istint
und der Typ vonb
istint *
(gelesen als "Zeiger aufint
"). In Ihrem Beispiel würde der Speicher Folgendes enthalten:..... memory address ...... value ........ type a ... 0x00000002 .......... 5 ............ int b ... 0x00000010 .......... 0x00000002 ... int*
Der Typ ist nicht wirklich im Speicher gespeichert. Der Compiler weiß nur, dass Sie beim Lesen
a
einen findenint
und beim Lesenb
die Adresse eines Ortes, an dem Sie einen finden könnenint
.In Ihrem zweiten Beispiel:
int a = 5; int *b = &a; int **c = &b;
Der Typ von
c
isint **
wird als "Zeiger auf Zeiger aufint
" gelesen . Für den Compiler bedeutet dies:c
ist ein Zeiger;c
, erhalten Sie die Adresse eines anderen Zeigers.int
.Das ist,
c
ist ein Zeiger (int **
);*c
ist auch ein Zeiger (int *
);**c
ist einint
.Und die Erinnerung würde enthalten:
..... memory address ...... value ........ type a ... 0x00000002 .......... 5 ............ int b ... 0x00000010 .......... 0x00000002 ... int* c ... 0x00000020 .......... 0x00000010 ... int**
Da der "Typ" nicht zusammen mit dem Wert gespeichert wird und ein Zeiger auf eine beliebige Speicheradresse verweisen kann, kennt der Compiler den Typ des Werts an einer Adresse im Wesentlichen, indem er den Typ des Zeigers nimmt und ganz rechts entfernt
*
.Das ist übrigens für eine gängige 32-Bit-Architektur. Für die meisten 64-Bit-Architekturen haben Sie:
..... memory address .............. value ................ type a ... 0x0000000000000002 .......... 5 .................... int b ... 0x0000000000000010 .......... 0x0000000000000002 ... int* c ... 0x0000000000000020 .......... 0x0000000000000010 ... int**
Adressen sind jetzt jeweils 8 Bytes, während an
int
nur noch 4 Bytes sind. Da der Compiler den Typ jeder Variablen kennt , kann er diesen Unterschied leicht bewältigen und 8 Bytes für einen Zeiger und 4 Bytes für den Zeiger lesenint
.quelle
Der
&
Operator gibt einen Zeiger auf das Objekt aus, das&a
vom Typ ist,int *
sodass die Zuweisung (durch Initialisierung) des Objekts, dasb
ebenfalls vom Typint *
ist, gültig ist.&b
ergibt einen Zeiger auf Objektb
, das heißt&b
ist vom Typ Zeigerint *
, d .h.,int **
.C sagt in den Einschränkungen des Zuweisungsoperators (die für die Initialisierung gelten), dass (C11, 6.5.16.1p1): "Beide Operanden sind Zeiger auf qualifizierte oder nicht qualifizierte Versionen kompatibler Typen" . Aber in der C-Definition, was ein kompatibler Typ ist
int **
und wasint *
nicht kompatible Typen sind.Daher liegt bei der
int *c = &b;
Initialisierung eine Einschränkungsverletzung vor, was bedeutet, dass der Compiler eine Diagnose benötigt.Eine der Begründungen der Regel ist, dass der Standard nicht garantiert, dass die beiden unterschiedlichen Zeigertypen dieselbe Größe haben (mit Ausnahme
void *
der Zeichenzeigertypen und der Zeichenzeigertypen), dh unterschiedliche Werte habensizeof (int *)
und seinsizeof (int **)
können.quelle
Dies liegt daran, dass jeder Zeiger
T*
tatsächlich vom Typpointer to a T
(oderaddress of a T
) ist, wobeiT
es sich um den Typ handelt, auf den verwiesen wird. In diesem Fall*
kann als gelesenpointer to a(n)
werden undT
ist der Typ , auf den verwiesen wird.int x; // Holds an integer. // Is type "int". // Not a pointer; T is nonexistent. int *px; // Holds the address of an integer. // Is type "pointer to an int". // T is: int int **pxx; // Holds the address of a pointer to an integer. // Is type "pointer to a pointer to an int". // T is: int*
Dies wird für Dereferenzierungszwecke verwendet, bei denen der Dereferenzierungsoperator a
T*
nimmt und einen Wert zurückgibt , dessen Typ istT
. Der Rückgabetyp kann so gesehen werden, dass der am weitesten links stehende "Zeiger auf a (n)" abgeschnitten wird und alles übrig bleibt.*x; // Invalid: x isn't a pointer. // Even if a compiler allows it, this is a bad idea. *px; // Valid: px is "pointer to int". // Return type is: int // Truncates leftmost "pointer to" part, and returns an "int". *pxx; // Valid: pxx is "pointer to pointer to int". // Return type is: int* // Truncates leftmost "pointer to" part, and returns a "pointer to int".
Beachten Sie, dass für jede der oben genannten Operationen der Rückgabetyp des Dereferenzierungsoperators mit dem Typ der ursprünglichen
T*
Deklaration übereinstimmtT
.Dies hilft sowohl primitiven Compilern als auch Programmierern beim Parsen des Zeigertyps erheblich: Bei einem Compiler fügt der Operator address-of
*
dem Typ ein hinzu, der Dereferenzierungsoperator entfernt a*
aus dem Typ, und jede Nichtübereinstimmung ist ein Fehler. Für einen Programmierer ist die Anzahl von*
s ein direkter Hinweis darauf, mit wie vielen Indirektionsebenen Sie es zu tun haben (zeigtint*
immer aufint
, zeigtfloat**
immer auffloat*
welche wiederum immer zeigtfloat
usw.).In Anbetracht dessen gibt es zwei Hauptprobleme, bei denen
*
unabhängig von der Anzahl der Indirektionsebenen nur eine einzige verwendet wird :In beiden Fällen besteht die einzige Möglichkeit, den tatsächlichen Typ des Werts zu bestimmen, darin, ihn zurückzuverfolgen und Sie zu zwingen, woanders zu suchen, um ihn zu finden.
void f(int* pi); int main() { int x; int *px = &x; int *ppx = &px; int *pppx = &ppx; f(pppx); } // Ten million lines later... void f(int* pi) { int i = *pi; // Well, we're boned. // To see what's wrong, see main(). }
Dies ... ist ein sehr gefährliches Problem, das leicht gelöst werden kann, indem die Anzahl der
*
s direkt die Indirektionsebene darstellt.quelle