Zeiger auf Zeiger vs. normale Zeiger

74

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;
user42298
quelle
13
Darf ich neben so vielen guten Antworten bitte einen einfachen Kommentar posten? Der Clang-Compiler gibt diese eindeutige Warnung aus, wenn er versucht, den fraglichen Teil Ihres Codes zu kompilieren : warning: incompatible pointer types initializing 'int *' with an expression of type 'int **'; remove & [-Wincompatible-pointer-types]. Dies könnte alles klar gemacht haben.
user3078414
14
Anfänger sind oft verwirrt, weil sie "Adressen" als Datentyp an sich betrachten. Sie sind es nicht. Adressen von Daten vom Typ X sind. Und sie sind für verschiedene Typen unterschiedlich. Dies führte dazu, dass Sie glaubten, int * und int ** seien gleich.
Michel Billaud
4
" Wenn der Zweck des Zeigers nur darin besteht, die Speicheradresse zu speichern ", ist dies nicht der Fall . Der Zweck eines Zeigers besteht darin, die "Speicheradresse" eines Objekts zusammen mit seinem Typ zu speichern. Starten Sie einfach die Dereferenzierung der Zeiger und Sie werden sehen.
Margaret Bloom
8
Tut mir leid, nicht unhöflich zu sein, sondern mich nur zu fragen, was macht diese Frage so nützlich? Dies ist in jedem gemäßigten C-Buch enthalten, Zeiger Kapitel zweiten oder dritten Artikel, maximal, ganz zu schweigen davon, viele Male in SO diskutiert. Habe ich etwas Offensichtliches verpasst?
Sourav Ghosh
3
@SouravGhosh der Typ hat wahrscheinlich seine Frage für Repräsentanten verstärkt, indem er sie von einem gut besiedelten Anfängerforum verlinkt hat
MM

Antworten:

90

Im

int a = 5;
int *b = &a;   
int *c = &b;

Sie erhalten eine Warnung, weil sie &bvom Typ ist int **, und Sie versuchen, eine Variable vom Typ zu initialisieren int *. 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 Compiler inteinen Zeiger, den wir weiter dereferenzieren können.

Beachten Sie auch, dass auf vielen Systemen intund int*nicht dieselbe Größe vorhanden ist (z. B. kann ein Zeiger 64 Bit lang und int32 Bit lang sein). Wenn Sie dereferenzieren fund eine erhalten int, verlieren Sie die Hälfte des Werts und können ihn dann nicht einmal in einen gültigen Zeiger umwandeln.

Ein Programmierer
quelle
4
Und auf Systemen, für die C entwickelt wurde (sowie auf einigen modernen eingebetteten Systemen), gab es verschiedene Arten von Zeigern im selben Programm - zum Beispiel Nah- und Fernzeiger oder Daten- und Codezeiger. Zeiger ist kein int-Wert, Leute; Hör auf so zu tun, als wäre es nur, weil es "meistens funktioniert" .
Luaan
2
@Luaan: C wurde für die frühen DEC-Minicomputer entwickelt und war ab den 1970er Jahren auf dem PDP-11 am verbreitetsten. Diese Maschinen hatten KEINE "nahen" und "fernen" Zeiger. "Nah" und "Fern" waren eine Microsoft-Erweiterung für C, um die hirntote segmentierte Intel 8086/8088-Architektur auf dem IBM PC zu unterstützen. (Zu Beginn des PC-Projekts produzierte und verkaufte IBM einen Motorola 68000-basierten Laborcomputer an einem anderen Standort. Stellen Sie sich die Einsparungen bei Aspirin allein vor, die durch die Vermeidung von Segmentkopfschmerzen verursacht wurden, wenn die beiden Standorte miteinander gesprochen hätten ...)
John R. Strohm
1
@Luaan nicht wirklich wahr. Erst als andere Anbieter und ANSI / ISO es in die Hände bekamen, wurde diese Art von Portabilitätsproblemen angesprochen. 1972 war C vollkommen zuversichtlich, Zeiger und Ints auszutauschen. beide waren 16-Bit und Zeiger waren flach. void *gab es nicht einmal, man konnte es immer nur benutzen char *:)
hobbs
53

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 Variable, Zeiger, Doppelzeiger usw. bezieht

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*und int**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);
alain
quelle
Wenn ich das richtig verstehe, gibt der Zeigertyp an, wie viele Speicher die CPU ab der verweisenden Speicheradresse liest. Der Ausdruck 'int * b = & a; printf ("% d ', * b);' bedeutet, dass wir ausgehend von der Adresse a 4 Byte lesen. Dies ist möglich, weil wir die Größe von int als 4 Byte definiert und diese Interger gedruckt haben. Aber wie groß ist sie dann? von int *, int **, int ***? Unterscheidet sich das von System zu System? Was (Compiler, CPU oder andere?) definiert dann seine Größe?
user42298
1
Die Größe des Typs ist nur eine Überlegung. Da sind andere.
Gregory Currie
Betrachten Sie einen Zeiger auf etwas p. Stellen Sie sich nun vor, Sie möchten den Wert speichern, auf den p zeigt. int i = * p; Der Compiler muss den Typ des Zeigers kennen. Im Speicher sieht ein Double ganz anders aus als ein Int. Dies hat nichts mit der Größe des Zeigers zu tun, sondern damit, wie die Daten behandelt werden sollen, auf die verwiesen wird.
Gregory Currie
1
@ user42298 Die Größe der Zeiger hängt normalerweise von der CPU und dem Compiler ab. Ein für 64 Bit kompiliertes Programm, das auf einer 64-Bit-CPU ausgeführt wird, verfügt über 64 Bit breite Zeiger. Sie können aber auch 32-Bit kompilieren und auf einer 64-Bit-CPU ausführen. Dann sind die Zeiger 32-Bit, und das Betriebssystem sorgt dafür, dass dies korrekt ausgeführt wird. Übrigens intkann auch eine andere Größe haben, hat aber int32_tgarantiert genau 32 Bit.
Alain
Dies ist die umfassendste Antwort unter den anderen.
Edmz
23

Meine Frage ist nun, warum diese Art von Code funktioniert.

int a = 5; 
int *b = &a; 
int *c = &b; 

eine Warnung generieren?

Sie müssen zu den Grundlagen zurückkehren.

  • Variablen haben Typen
  • Variablen enthalten Werte
  • Ein Zeiger ist ein Wert
  • Ein Zeiger bezieht sich auf eine Variable
  • Wenn pes sich um einen Zeigerwert handelt, *phandelt es sich um eine Variable
  • Wenn ves sich um eine Variable &vhandelt, handelt es sich um einen Zeiger

Und jetzt können wir alle Fehler in Ihrem Beitrag finden.

Dann gehe ich davon aus, dass ich jetzt die Adresse des Zeigers speichern möchte *b

Nr. *bIst eine Variable vom Typ int. Es ist kein Zeiger. bist eine Variable, deren Wert ein Zeiger ist. *bist eine Variable, deren Wert eine Ganzzahl ist.

**cbezieht sich auf die Adresse von *b.

NEIN NEIN NEIN. Absolut nicht. Sie müssen dies richtig verstehen, wenn Sie Zeiger verstehen wollen.

*bist eine Variable; Es ist ein Alias ​​für die Variable a. Die Adresse der Variablen aist der Wert der Variablen b. **cbezieht sich nicht auf die Adresse von a. Es ist vielmehr eine Variable , die ein Alias für eine Variable ist a. (Und so ist es auch *b.)

Die richtige Aussage lautet: Der Wert der Variablen cist die Adresse von b. Oder äquivalent: Der Wert von cist ein Zeiger, auf den verwiesen wird b.

Woher wissen wir das? Gehen Sie zurück zu den Grundlagen. Das hast du gesagt c = &b. Was ist der Wert von c? 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.

Eric Lippert
quelle
Ich denke, jedes Mal, wenn OP in der Post b oder ** c sagt , versuchen sie wirklich, nur b und c zu sagen. Sie behandeln das viel, aber nicht wirklich den letzten Teil der Frage ("warum kann int nicht auf ein anderes int * zeigen"), was OP wirklich versucht, IMO zu fragen.
mbrig
Wessen geniale Idee war es, den Dereferenzierungsoperator und das Qualifikationsmerkmal "ist ein Zeiger" sowieso zum gleichen Symbol zu machen ...
mbrig
6
@mbrig: Es war Dennis Ritchies geniale Idee. Wenn nicht klar ist, warum dies eine geniale Idee ist, analysieren Sie die Sprache mental nicht richtig. Wenn wir sagen , int * b;wir sind nicht nur sagen " bist eine Variable vom Typ int*. Wir sagen auch , dass *beine Variable vom Typ istint die hier geniale Idee ist , dass Sie geistig als beide betrachten können. int* b Und int *bund entweder Interpretation korrekt ist .
Eric Lippert
Ja, ich habe mir einige verknüpfte Fragen angesehen, und das machte für mich zum ersten Mal Sinn. Ich stelle es trotzdem etwas in Frage.
mbrig
20

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, ist int*der Typ ähnlich int. Bei Ihrem Vorschlag wäre der Typ nicht eindeutig.

Anhand Ihres Beispiels ist es unmöglich zu wissen, ob cPunkte auf a intoder int*:

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:

*c;

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.

2501
quelle
17

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 *und int **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 pvom Typ ergibt T *der Ausdruck p + 1die Adresse des nächsten Objekts vom Typ T. 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 + 1gibt uns die Adresse des nächsten charObjekts, das wäre 0x1001. Der Ausdruck sp + 1gibt uns die Adresse des nächsten shortObjekts, das wäre 0x1002. ip + 1gibt uns 0x1004und lp + 1gibt uns 0x1008.

Also gegeben

int a = 5;
int *b = &a;
int **c = &b;

b + 1gibt uns die Adresse des nächsten intund c + 1gibt uns die Adresse des nächsten Zeigers auf int.

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 TypT . Wenn wir durch Teinen Zeigertyp ersetzen P *, wird der Code

void 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 pist immer eine Indirektionsebene mehr als die Variable val.

John Bode
quelle
11

Ich denke, es sollte keine Hierarchie geben, wenn die Adresse, die wir speichern wollen, auf Variable, Zeiger, Doppelzeiger verweist

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 *pcliest das , charals 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.

4386427
quelle
10

Wenn der Zweck des Zeigers nur darin besteht, die Speicheradresse zu speichern, sollte es meiner Meinung nach keine Hierarchie geben, wenn die Adresse, die wir speichern möchten, auf Variablen, Zeiger, Doppelzeiger usw. verweist. Daher sollte der folgende Codetyp gültig sein.

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;

&aimpliziert, dass das, was Sie erhalten, ein ist, int *so dass, wenn Sie dereferenzieren, es intwieder ein ist.

Bringen Sie das auf die nächsten Ebenen,

int *b = &a;
int **c = &b;

&bist 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 ein int *und **(&b)der ursprüngliche intWert, 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.

glglgl
quelle
9

Wenn der Zweck des Zeigers nur darin besteht, die Speicheradresse zu speichern, sollte es meiner Meinung nach keine Hierarchie geben, wenn die Adresse, die wir speichern möchten, auf Variablen, Zeiger, Doppelzeiger usw. verweist. Daher sollte der folgende Codetyp gültig sein.

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 ...

Jean-Baptiste Yunès
quelle
Dies gilt auch nicht unbedingt für die Maschine. In älteren Systemen (und einigen modernen eingebetteten Systemen) gab es verschiedene Arten von Zeigern. Beispielsweise verfügt die x86-Architektur über Zeiger in der Nähe (16 Bit) und in der Ferne (32 Bit). C verbirgt diese Tatsache vor Ihnen, aber es ist entscheidend, dass die Anwendung auf einem x86-Computer im 16-Bit-Modus ausgeführt wird. Es gibt auch andere Beispiele, zum Beispiel im segmentierten Modus (Segment + Offset, bei dem ein Nullzeiger im tatsächlichen Maschinencode nicht Null ist). C hat nicht viele Abstraktionen auf hoher Ebene, aber es hat viele Abstraktionen auf niedriger Ebene - das war der ganze Zweck von C.
Luaan
@Luaan: Es gibt viele Maschinen, auf denen Zeiger auf Funktionen und Zeiger auf Daten unterschiedlich sind. Maschinen, bei denen der Zieltyp eines Zeigers seine Größe beeinflusst, existieren, sind jedoch viel seltener. Es wäre nützlich, wenn C einen optionalen Typ "Zeiger auf irgendeine Art von Zeiger auf Daten" definieren würde, der bei Implementierungen definiert würde, bei denen alle Arten von Daten dieselbe Darstellung verwendeten, da derzeit die einzige Möglichkeit besteht, eine Funktion zu schreiben, mit der gearbeitet werden kann Alle diese Zeiger (z. B. zum Sortieren) dienen dazu, sie mit memcpy, memmove oder Zeichentypen und mit einer dieser Techniken zu manipulieren ...
supercat
... würde erfordern, dass ein Compiler die doppelt indirekten Zeiger als potenzielles Aliasing für jeden Typ (einschließlich Zeichen, Ganzzahlen und Gleitkommawerte) und nicht nur als Zeiger behandelt, aber die Autoren des C-Standards haben dies traditionell abgelehnt Definieren Sie alles, was nicht bei allen Implementierungen verarbeitet werden kann.
Supercat
All dies ist vollkommen richtig, ich weiß, es ist manchmal notwendig, die Dinge zu vereinfachen. Ich hätte gemeint, dass in rohen Maschinen die Dinge weniger streng sind als in Sprachen, und dass wir in einer Sprache nicht wie in rohen Maschinen denken können.
Jean-Baptiste Yunès
9

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.

Amin Negm-Awad
quelle
9

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 aist intund der Typ von bist int *(gelesen als "Zeiger auf int"). 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 aeinen finden intund beim Lesen bdie Adresse eines Ortes, an dem Sie einen finden können int.

In Ihrem zweiten Beispiel:

int a = 5;
int *b = &a;
int **c = &b;

Der Typ von cis int **wird als "Zeiger auf Zeiger auf int" gelesen . Für den Compiler bedeutet dies:

  • c ist ein Zeiger;
  • Wenn Sie lesen c, erhalten Sie die Adresse eines anderen Zeigers.
  • Wenn Sie diesen anderen Zeiger lesen, erhalten Sie die Adresse eines int.

Das ist,

  • cist ein Zeiger ( int **);
  • *cist auch ein Zeiger ( int *);
  • **cist ein int.

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 intnur 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 lesen int.

CesarB
quelle
6

Warum generiert diese Art von Code eine Warnung?

int a = 5;
int *b = &a;   
int *c = &b;

Der &Operator gibt einen Zeiger auf das Objekt aus, das &avom Typ ist, int *sodass die Zuweisung (durch Initialisierung) des Objekts, das bebenfalls vom Typ int *ist, gültig ist. &bergibt einen Zeiger auf Objekt b, das heißt &bist vom Typ Zeiger int *, 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 was int *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 haben sizeof (int *)und sein sizeof (int **)können.

ouah
quelle
4

Dies liegt daran, dass jeder Zeiger T*tatsächlich vom Typ pointer to a T(oder address of a T) ist, wobei Tes sich um den Typ handelt, auf den verwiesen wird. In diesem Fall *kann als gelesen pointer to a(n)werden und Tist 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 ist T. 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 übereinstimmt T.

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 (zeigt int*immer auf int, zeigt float**immer auf float*welche wiederum immer zeigt floatusw.).


In Anbetracht dessen gibt es zwei Hauptprobleme, bei denen *unabhängig von der Anzahl der Indirektionsebenen nur eine einzige verwendet wird :

  1. Der Zeiger ist für den Compiler viel schwieriger zu dereferenzieren, da er auf die letzte Zuweisung zurückgreifen muss, um den Grad der Indirektion zu bestimmen und den Rückgabetyp entsprechend zu bestimmen.
  2. Der Zeiger ist für den Programmierer schwieriger zu verstehen, da er leicht den Überblick darüber verliert, wie viele Indirektionsebenen es gibt.

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.

Justin Time - Monica wieder einsetzen
quelle