Frage Zeiger auf Zeiger gegenüber normalen Zeigern


Der Zweck eines Zeigers besteht darin, die Adresse einer bestimmten Variablen zu speichern. Dann sollte die Speicherstruktur des folgenden Codes 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 von Zeiger * b speichern möchte. Dann definieren wir allgemein einen Doppelzeiger, ** c, als

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

Dann sieht die Speicherstruktur wie folgt aus:

...... Speicheradresse ...... Wert
  a ... 0x000002 ................... 5
  b ... 0x000010 ................... 0x000002
  c ... 0x000020 ................... 0x000010

So ** c bezieht sich auf die Adresse von * b.

Jetzt ist meine Frage, warum diese Art von Code,

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 Adresse, die wir speichern wollen, auf eine Variable, einen Zeiger, einen Doppelzeiger usw. bezieht gültig sein.

int a = 5;
int *b = &a;
int *c = &b;
int *d = &c;
int *e = &d;
int *f = &e;

74
2018-06-28 13:01


Ursprung


Antworten:


Im

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

Sie erhalten eine Warnung, weil &b ist vom Typ int **, und Sie versuchen, eine Variable des Typs zu initialisieren int *. Es gibt keine impliziten Konvertierungen zwischen diesen beiden Typen, die zu der Warnung führen.

Um das längere Beispiel zu nehmen, das Sie arbeiten möchten, wenn wir versuchen, zu dereferenzieren f Der Compiler wird uns einen geben int, kein Zeiger, den wir weiter dereferenzieren können.

Beachten Sie auch das auf vielen Systemen int und int* sind nicht die gleiche Größe (z.B. kann ein Zeiger 64 Bits lang sein und ein int 32 Bits lang). Wenn Sie dereferenzieren f und bekomme ein int, verlieren Sie die Hälfte des Wertes, und dann können Sie nicht einmal auf einen gültigen Zeiger werfen.


91
2018-06-28 13:04



Wenn der Zweck des Zeigers nur darin besteht, die Speicheradresse zu speichern, denke ich   Es sollte keine Hierarchie geben, wenn die Adresse gespeichert wird   verweist auf Variable, Zeiger, Doppelzeiger, ... usw

Zur Laufzeit ja, ein Zeiger hält nur eine Adresse. Aber zur Kompilierzeit gibt es auch einen Typ für jede Variable. Wie die anderen gesagt haben, int* und int** sind zwei verschiedene, inkompatible Typen.

Es gibt einen Typ, void*, das tut, was Sie wollen: Es speichert nur eine Adresse, Sie können ihm eine beliebige Adresse zuweisen:

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

Aber wenn du eine dereferenzieren willst void*, müssen Sie die fehlenden Informationen selbst eingeben:

int a2 = **((int**)c);

54
2018-06-28 13:30



Jetzt ist meine Frage, warum diese Art von Code,

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 verweist auf eine Variable
  • ob p ist dann ein Zeigerwert *p ist eine Variable
  • ob v ist dann eine Variable &v ist ein Zeiger

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

Dann nehme an, dass ich jetzt die Adresse des Zeigers speichern möchte *b

Nein. *b ist eine Variable vom Typ int. Es ist kein Zeiger. b ist eine Variable, deren Wert ist ein Zeiger. *b ist ein Variable dessen Wert eine Ganzzahl ist.

**c bezieht sich auf die Adresse von *b.

NEIN NEIN NEIN. Absolut nicht. Sie haben um das richtig zu verstehen, wenn Sie Zeiger verstehen wollen.

*b ist eine Variable; Es ist ein Alias ​​für die Variable a. Die Adresse der Variablen a ist der Wert der Variablen b. **c bezieht sich nicht auf die Adresse von a. Es ist vielmehr ein Variable das ist ein alias für Variable a. (Und so ist *b.)

Die richtige Aussage ist: der Wert der Variablen c ist der Adresse von b. Oder, äquivalent: der Wert von c ist ein Zeiger, auf den sich bezieht b.

Woher wissen wir das? Geh zurück zu den Grundlagen. Du hast das gesagt c = &b. Also, was ist der Wert von? c? Ein Zeiger. Um was? b.

Stell sicher, dass du völlig verstehe die grundlegenden Regeln.

Nun, da Sie hoffentlich die korrekte Beziehung zwischen Variablen und Zeigern verstehen, sollten Sie in der Lage sein, Ihre Frage zu beantworten, warum Ihr Code einen Fehler verursacht.


23
2018-06-28 20:51



Das Typsystem von C erfordert dies, wenn Sie eine korrekte Warnung erhalten möchten und der Code überhaupt kompilieren soll. Mit nur einer Ebene der Tiefe von Zeigern wüssten Sie nicht, ob der Zeiger auf einen Zeiger oder auf eine tatsächliche Ganzzahl zeigt.

Wenn Sie einen Typ dereferenzieren int** Du kennst den Typ, den du bekommst int* und in ähnlicher Weise, wenn Sie dereferenzieren int* der Typ ist int. Mit Ihrem Vorschlag wäre der Typ mehrdeutig.

Aus Ihrem Beispiel ist es unmöglich zu wissen, ob c zeigt auf a int oder int*:

c = rand() % 2 == 0 ? &a : &b;

Auf welchen Typ zeigt c? Der Compiler weiß das nicht, daher ist diese nächste Zeile unmöglich auszuführen:

*c;

In C gehen alle Typinformationen nach dem Kompilieren verloren, da jeder Typ zur Kompilierzeit ü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 über die in Zeigern enthaltenen Typen haben müsste.


20
2018-06-28 13:06



Zeiger sind Abstraktionen von Speicheradressen mit zusätzlicher Typ-Semantik und in einer Sprache wie C-Typ von Bedeutung.

Vor allem gibt es keine Garantie dafür int * und int ** haben die gleiche Größe oder Darstellung (auf modernen Desktop-Architekturen tun sie das, aber man kann sich nicht darauf verlassen, dass es allgemein wahr ist).

Zweitens spielt der Typ für die Zeigerarithmetik eine Rolle. Einen Zeiger gegeben p vom Typ T *, der Ausdruck p + 1 ergibt die Adresse des nächsten Objekt vom Typ T. Nehmen wir also die folgenden Deklarationen 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ächsten char Objekt, das wäre 0x1001. Der Ausdruck sp + 1 gibt uns die Adresse des nächsten short Objekt, das wäre 0x1002. ip + 1 gibt uns 0x1004, und lp + 1 gibt uns 0x1008.

Also, gegeben

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

b + 1 gibt uns die Adresse des nächsten int, und c + 1 gibt uns die Adresse des nächsten Zeiger zu int.

Pointer-to-Pointer sind erforderlich, wenn eine Funktion in einen Parameter vom Zeigertyp schreiben soll. Nimm 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 jeder Typ T. Wenn wir ersetzen T mit einem Zeigertyp 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 dieselbe, nur die Typen sind unterschiedlich; der formale Parameter pist immer eine weitere Ebene der Indirektion als die Variable val.


17
2018-06-28 14:41



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 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 dadurch hilft es mir zu wissen, dass ich etwas falsch gemacht habe und ich den Fehler korrigieren 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 die char als ob es ein Zeiger wäre, d.h. wahrscheinlich 4 oder 8 Bytes liest, obwohl wir nur 1 Byte reserviert haben. Das ist UB.

Wenn das Programm aufgrund der obigen UB nicht abstürzte, sondern nur einen gewissen Wert zurückgab, würde der zweite Schritt darin bestehen, den Wert zu demeferenzieren. Noch einmal UB.

Fazit

Das Typsystem hilft uns, Fehler zu erkennen, indem wir int *, int **, int *** usw. als unterschiedliche Typen sehen.


11
2018-06-28 13:56



Wenn der Zweck des Zeigers nur darin besteht, die Speicheradresse zu speichern, denke ich, sollte es keine Hierarchie geben, wenn die Adresse, die wir speichern werden, Variable, Zeiger, Doppelzeiger, ... usw. unterstellt, so sollte der Typ des Codes gültig sein.

Ich denke hier ist dein Missverständnis: Der Zweck des Zeigers ist es, die Speicheradresse zu speichern, aber ein Zeiger hat normalerweise auch einen Typ, so dass wir wissen, was wir an dem Ort erwarten, auf den er zeigt.

Insbesondere möchten andere Leute diese Art von Hierarchie wirklich haben, um zu wissen, was mit dem Speicherinhalt zu tun ist, auf den der Zeiger zeigt.

Es ist der eigentliche Punkt des Zeigersystems von C, an den Typinformationen angehängt zu sein.

Wenn Sie tun

int a = 5;

&a impliziert, dass das, was Sie bekommen, ist a int * so dass, wenn Sie dereferenzieren, es ein ist int nochmal.

Bring 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, es ist nutzlos. Es ist wichtig zu wissen, dass die Dereferenzierung eines Zeigers den Typ des Originaltyps anzeigt, so dass *(&b) ist ein int *, und **(&b) ist das Original int 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 ziemlich begrenzt ist.


10
2018-06-28 13:09



Wenn der Zweck des Zeigers nur darin besteht, die Speicheradresse zu speichern, denke ich, sollte es keine Hierarchie geben, wenn die Adresse, die wir speichern werden, Variable, Zeiger, Doppelzeiger, ... usw. unterstellt, so sollte der Typ des Codes gültig sein.

Nun, das gilt für die Maschine (immerhin ist alles eine Nummer). Aber in vielen Sprachen werden Variablen eingegeben, was bedeutet, dass der Compiler dann sicherstellen kann, dass Sie sie korrekt verwenden (Typen geben Variablen einen korrekten Kontext vor)

Es ist richtig, dass ein Zeiger auf Zeiger und ein Zeiger (wahrscheinlich) die gleiche Menge an Speicher verwenden, um ihren Wert zu speichern (Achtung, dies gilt nicht für int und Zeiger auf int, die Größe einer Adresse steht nicht mit der Größe von a in Beziehung Haus).

Wenn Sie also eine Adresse einer Adresse haben, sollten Sie sie so verwenden, wie sie ist, und nicht als einfache Adresse, denn wenn Sie auf den Pointer als einfachen Pointer zugreifen, könnten Sie eine Adresse von int so manipulieren, als wäre sie ein int , das ist nicht (ersetzen int ohne etwas anderes und Sie sollten die Gefahr sehen). Sie sind vielleicht verwirrt, weil das alles Zahlen sind, aber im täglichen Leben nicht: Ich persönlich mache einen großen Unterschied in $ 1 und 1 Hund. Hund und $ sind Typen, Sie wissen, was Sie mit ihnen machen können.

Sie können in Assembly programmieren und machen was Sie wollen, aber Sie werden sehen, wie gefährlich es ist, weil Sie fast das tun können, was Sie wollen, besonders komische Dinge. Ja die Änderung eines Adresswertes ist gefährlich. Angenommen, Sie haben ein autonomes Auto, das etwas an einer Adresse in der Entfernung liefern soll: 1200 Speicher Straße (Adresse) und angenommen in dieser Straße sind die Häuser durch 100 Fuß getrennt (1221 ist eine ungültige Adresse), Wenn Sie in der Lage sind, Adressen beliebig als Ganzzahl zu manipulieren, könnten Sie versuchen, bei 1223 zu liefern und das Paket in der Mitte des Straßenpflasters zu lassen.

Ein anderes Beispiel könnte Haus, Adresse des Hauses, Eintragsnummer in einem Adressbuch dieser Adresse sein. All diese drei sind unterschiedliche Konzepte, verschiedene Arten ...


9
2018-06-28 13:46



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 zu Zeigern werden gespeichert, aber zu was sie beziehen sich.


9
2018-06-28 19:17



Die C-Sprache ist stark typisiert. Das heißt, für jede Adresse gibt es a Art, der dem Compiler sagt, wie der Wert an dieser Adresse zu interpretieren ist.

In Ihrem Beispiel:

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

Die Art von a ist intund die Art von b ist int * (lies als "Zeiger auf int"). In Ihrem Beispiel würde der Speicher enthalten:

..... memory address ...... value ........ type
a ... 0x00000002 .......... 5 ............ int
b ... 0x00000010 .......... 0x00000002 ... int*

Das Art ist nicht wirklich im Speicher gespeichert, es ist nur, dass der Compiler das weiß, wenn Sie lesen a Du wirst ein finden intund wenn du liest b Sie finden die Adresse eines Ortes, an dem Sie eine finden können int.

In Ihrem zweiten Beispiel:

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

Die Art von c ist int **, gelesen als "Zeiger auf Zeiger auf int". Es bedeutet, dass für den Compiler:

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

Das ist,

  • c ist ein Zeiger (int **);
  • *c ist auch ein Zeiger (int *);
  • **c ist 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 zeigen kann, ist die Art und Weise, wie der Compiler den Typ des Wertes an einer Adresse kennt, im Grunde genommen der Typ des Zeigers, und das Entfernen des ganz rechten *.


Übrigens, das ist für eine gewöhnliche 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**

Die Adressen sind jetzt 8 Bytes, während ein int ist immer noch nur 4 Bytes. Da der Compiler das kennt Art von jeder Variable kann er leicht mit dieser Differenz umgehen und liest 8 Bytes für einen Zeiger und 4 Bytes für die int.


9
2018-06-29 17:30