Wie funktionieren Zeiger auf Zeiger in C?

171

Wie funktionieren Zeiger auf Zeiger in C? Wann würden Sie sie verwenden?

entspannen
quelle
43
Nein, keine Hausaufgaben ... wollte nur wissen ... weil ich es oft sehe, wenn ich C-Code lese.
1
Ein Zeiger auf Zeiger ist kein Sonderfall von etwas, daher verstehe ich nicht, was Sie über void ** nicht verstehen.
Akappa
Für 2D-Arrays ist das beste Beispiel, dass die Befehlszeilenargumente "prog arg1 arg2" char ** argv gespeichert sind. Und wenn der Anrufer den Speicher nicht zuordnen möchte (die aufgerufene Funktion ordnet den Speicher zu)
Ergebnisweg
1
Sie haben ein schönes Beispiel für die Verwendung von "Zeiger auf Zeiger" in Git 2.0: siehe meine Antwort unten
VonC

Antworten:

359

Nehmen wir einen 8-Bit-Computer mit 8-Bit-Adressen (und damit nur 256 Byte Speicher) an. Dies ist Teil dieses Speichers (die Zahlen oben sind die Adressen):

  54   55   56   57   58   59   60   61   62   63   64   65   66   67   68   69
+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
|    | 58 |    |    | 63 |    | 55 |    |    | h  | e  | l  | l  | o  | \0 |    |
+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+

Was Sie hier sehen können, ist, dass bei Adresse 63 die Zeichenfolge "Hallo" beginnt. Wenn dies in diesem Fall das einzige Vorkommen von "Hallo" im Speicher ist, dann

const char *c = "hello";

... definiert cals Zeiger auf die (schreibgeschützte) Zeichenfolge "Hallo" und enthält somit den Wert 63. cmuss selbst irgendwo gespeichert werden: im obigen Beispiel an Position 58. Natürlich können wir nicht nur auf Zeichen zeigen , sondern auch auf andere Hinweise. Z.B:

const char **cp = &c;

Zeigt nun cpauf c, das heißt, es enthält die Adresse von c(die 58 ist). Wir können noch weiter gehen. Erwägen:

const char ***cpp = &cp;

cppSpeichert jetzt die Adresse von cp. Es hat also den Wert 55 (basierend auf dem obigen Beispiel), und Sie haben es erraten: Es ist selbst unter der Adresse 60 gespeichert.


In Bezug auf , warum ein Zeiger auf Zeiger verwendet:

  • Der Name eines Arrays ergibt normalerweise die Adresse seines ersten Elements. Wenn das Array also Elemente vom Typ enthält t, hat ein Verweis auf das Array den Typ t *. Betrachten Sie nun ein Array von Arrays vom Typ t: Natürlich hat ein Verweis auf dieses 2D-Array den Typ (t *)*= t **und ist daher ein Zeiger auf einen Zeiger.
  • Obwohl ein Array von Strings eindimensional klingt, ist es tatsächlich zweidimensional, da Strings Zeichenarrays sind. Daher : char **.
  • Eine Funktion fmuss ein Argument vom Typ akzeptieren, t **wenn eine Variable vom Typ geändert werden soll t *.
  • Viele andere Gründe, die zu zahlreich sind, um sie hier aufzulisten.
Stephan202
quelle
7
Ja, gutes Beispiel. Ich verstehe, was sie sind. Aber wie und wann man sie verwendet, ist wichtiger. Jetzt.
2
Stephan hat im Grunde genommen das Diagramm in Kernighan & Richies Programmiersprache The C gut reproduziert. Wenn Sie C programmieren und dieses Buch nicht haben und mit Papierdokumentation cool sind, empfehle ich Ihnen dringend, es zu bekommen. Die (ziemlich) bescheidenen Kosten machen sich sehr schnell in der Produktivität bezahlt. Es neigt dazu, in seinen Beispielen sehr klar zu sein.
J. Polfer
4
char * c = "Hallo" sollte const char * c = "Hallo" sein. Es ist auch höchstens irreführend zu sagen, dass "ein Array als Adresse des ersten Elements gespeichert ist". Ein Array wird als ... Array gespeichert. Oft gibt sein Name einen Zeiger auf sein erstes Element, aber nicht immer. Über Zeiger auf Zeiger würde ich einfach sagen, dass sie nützlich sind, wenn eine Funktion einen als Parameter übergebenen Zeiger ändern muss (dann übergeben Sie stattdessen einen Zeiger auf den Zeiger).
Bastien Léonard
4
Wenn ich diese Antwort nicht falsch interpretiere, sieht sie falsch aus. c wird bei 58 gespeichert und zeigt auf 63, cp wird bei 55 gespeichert und zeigt auf 58, und cpp ist im Diagramm nicht dargestellt.
Thanatos
1
Sieht gut aus. Als kleines Problem war alles, was mich davon abhielt zu sagen: Großartiger Beitrag. Die Erklärung selbst war ausgezeichnet. Wechsel zu einer Abstimmung. (Vielleicht muss Stackoverflow Zeiger überprüfen?)
Thanatos
46

Wie funktionieren Zeiger auf Zeiger in C?

Zunächst ist ein Zeiger wie jede andere Variable eine Variable, die jedoch die Adresse einer Variablen enthält.

Ein Zeiger auf einen Zeiger ist wie jede andere Variable eine Variable, die jedoch die Adresse einer Variablen enthält. Diese Variable ist zufällig ein Zeiger.

Wann würden Sie sie verwenden?

Sie können sie verwenden, wenn Sie einen Zeiger auf einen Speicher auf dem Heap zurückgeben müssen, aber nicht den Rückgabewert verwenden.

Beispiel:

int getValueOf5(int *p)
{
  *p = 5;
  return 1;//success
}

int get1024HeapMemory(int **p)
{
  *p = malloc(1024);
  if(*p == 0)
    return -1;//error
  else 
    return 0;//success
}

Und du nennst es so:

int x;
getValueOf5(&x);//I want to fill the int varaible, so I pass it's address in
//At this point x holds 5

int *p;    
get1024HeapMemory(&p);//I want to fill the int* variable, so I pass it's address in
//At this point p holds a memory address where 1024 bytes of memory is allocated on the heap

Es gibt auch andere Verwendungszwecke, z. B. hat das Argument main () jedes C-Programms einen Zeiger auf einen Zeiger für argv, wobei jedes Element ein Array von Zeichen enthält, die die Befehlszeilenoptionen darstellen. Sie müssen jedoch vorsichtig sein, wenn Sie Zeiger von Zeigern verwenden, um auf zweidimensionale Arrays zu zeigen. Es ist besser, stattdessen einen Zeiger auf ein zweidimensionales Array zu verwenden.

Warum ist es gefährlich?

void test()
{
  double **a;
  int i1 = sizeof(a[0]);//i1 == 4 == sizeof(double*)

  double matrix[ROWS][COLUMNS];
  int i2 = sizeof(matrix[0]);//i2 == 240 == COLUMNS * sizeof(double)
}

Hier ist ein Beispiel für einen Zeiger auf ein zweidimensionales Array, das ordnungsgemäß ausgeführt wurde:

int (*myPointerTo2DimArray)[ROWS][COLUMNS]

Sie können jedoch keinen Zeiger auf ein zweidimensionales Array verwenden, wenn Sie eine variable Anzahl von Elementen für ROWS und COLUMNS unterstützen möchten. Aber wenn Sie vorher wissen, würden Sie ein zweidimensionales Array verwenden.

Brian R. Bondy
quelle
32

Ich mag dieses "reale" Codebeispiel für die Verwendung von Zeigern auf Zeiger in Git 2.0, Commit 7b1004b :

Linus hat einmal gesagt:

Ich wünschte tatsächlich, mehr Menschen würden die wirklich grundlegende Art der Codierung auf niedriger Ebene verstehen. Keine großen, komplexen Dinge wie die Suche nach sperrenlosen Namen, sondern einfach eine gute Verwendung von Zeigern zu Zeigern usw.
Ich habe zum Beispiel zu viele Leute gesehen, die einen einfach verknüpften Listeneintrag löschen, indem sie den Eintrag "prev" verfolgen , und dann, um den Eintrag zu löschen, tun Sie so etwas

if (prev)
  prev->next = entry->next;
else
  list_head = entry->next;

und wenn ich solchen Code sehe, gehe ich einfach zu "Diese Person versteht keine Zeiger". Und es ist leider ziemlich häufig.

Personen, die Zeiger verstehen, verwenden einfach einen " Zeiger auf den Eintragszeiger " und initialisieren diesen mit der Adresse des Listenkopfs. Und dann, wenn sie die Liste durchlaufen, können sie den Eintrag ohne Verwendung von Bedingungen entfernen, indem sie einfach a ausführen

*pp =  entry->next

http://i.stack.imgur.com/bpfxT.gif

Wenn wir diese Vereinfachung anwenden, verlieren wir 7 Zeilen aus dieser Funktion, selbst wenn wir 2 Kommentarzeilen hinzufügen.

-   struct combine_diff_path *p, *pprev, *ptmp;
+   struct combine_diff_path *p, **tail = &curr;

Chris weist in den Kommentaren zum 2016er Video " Linus Torvalds 'Double Pointer Problem " von Philip Buuck darauf hin .


Kumar weist in den Kommentaren auf den Blog-Beitrag " Linus über das Verstehen von Zeigern " hin, in dem Grisha Trubetskoy erklärt:

Stellen Sie sich vor, Sie haben eine verknüpfte Liste definiert als:

typedef struct list_entry {
    int val;
    struct list_entry *next;
} list_entry;

Sie müssen von Anfang bis Ende darüber iterieren und ein bestimmtes Element entfernen, dessen Wert dem Wert von to_remove entspricht.
Der naheliegendste Weg, dies zu tun, wäre:

list_entry *entry = head; /* assuming head exists and is the first entry of the list */
list_entry *prev = NULL;

while (entry) { /* line 4 */
    if (entry->val == to_remove)     /* this is the one to remove ; line 5 */
        if (prev)
           prev->next = entry->next; /* remove the entry ; line 7 */
        else
            head = entry->next;      /* special case - first entry ; line 9 */

    /* move on to the next entry */
    prev = entry;
    entry = entry->next;
}

Was wir oben tun, ist:

  • Durchlaufen der Liste bis zum Eintrag NULL, was bedeutet, dass wir das Ende der Liste erreicht haben (Zeile 4).
  • Wenn wir auf einen Eintrag stoßen, den wir entfernen möchten (Zeile 5),
    • Wir weisen den Wert des aktuellen nächsten Zeigers dem vorherigen zu.
    • Dadurch wird das aktuelle Element eliminiert (Zeile 7).

Es gibt oben einen Sonderfall: Zu Beginn der Iteration gibt es keinen vorherigen Eintrag ( previs NULL). Um den ersten Eintrag in der Liste zu entfernen, müssen Sie den Kopf selbst ändern (Zeile 9).

Was Linus sagte, ist, dass der obige Code vereinfacht werden könnte, indem das vorherige Element zu einem Zeiger auf einen Zeiger und nicht nur zu einem Zeiger gemacht wird .
Der Code sieht dann so aus:

list_entry **pp = &head; /* pointer to a pointer */
list_entry *entry = head;

while (entry) {
    if (entry->val == to_remove)
        *pp = entry->next;

    pp = &entry->next;
    entry = entry->next;
}

Der obige Code ist der vorherigen Variante sehr ähnlich, aber beachten Sie, dass wir nicht mehr auf den Sonderfall des ersten Elements der Liste achten müssen, da er ppnicht NULLam Anfang steht. Einfach und clever.

Außerdem hat jemand in diesem Thread kommentiert, dass der Grund dafür besser ist, weil *pp = entry->nextes atomar ist. Es ist mit Sicherheit NICHT atomar .
Der obige Ausdruck enthält zwei Dereferenzierungsoperatoren ( *und ->) und eine Zuweisung, und keines dieser drei Dinge ist atomar.
Dies ist ein weit verbreitetes Missverständnis, aber leider sollte so gut wie nichts in C als atomar angenommen werden (einschließlich der ++und --Operatoren)!

VonC
quelle
4
Dies wird helfen, besser zu verstehen - grisha.org/blog/2013/04/02/linus-on-understanding-pointers
Kumar
@ Kumar gute Referenz. Ich habe es in die Antwort für mehr Sichtbarkeit aufgenommen.
VonC
Dieses Video war für mich wichtig, um Ihr Beispiel zu verstehen. Insbesondere war ich verwirrt (und kriegerisch), bis ich ein Speicherdiagramm zeichnete und den Fortschritt des Programms verfolgte. Trotzdem scheint es mir immer noch etwas mysteriös.
Chris
@ Chris Tolles Video, danke, dass du es erwähnt hast! Ich habe Ihren Kommentar zur besseren Sichtbarkeit in die Antwort aufgenommen.
VonC
14

Als wir Hinweise auf einen Programmierkurs an der Universität gaben, erhielten wir zwei Hinweise, wie wir anfangen sollten, etwas über sie zu lernen. Das erste war, Pointer Fun With Binky anzusehen . Das zweite war, über die Passage der Haddocks 'Eyes aus Lewis Carrolls Through the Looking-Glass nachzudenken

"Du bist traurig", sagte der Ritter in einem besorgten Ton: "Lass mich dir ein Lied singen, um dich zu trösten."

"Ist es sehr lang?" Fragte Alice, denn sie hatte an diesem Tag viel Poesie gehört.

„Es ist lang“, sagte der Ritter, „aber es ist sehr, sehr schön. Jeder, der mich singen hört - entweder bringt es die Tränen in die Augen oder sonst - “

"Oder was?" sagte Alice, denn der Ritter hatte plötzlich eine Pause gemacht.

„Oder auch nicht, weißt du? Der Name des Songs heißt 'Haddocks' Eyes '. "

"Oh, das ist der Name des Liedes, oder?", Sagte Alice und versuchte sich interessiert zu fühlen.

„Nein, du verstehst nicht“, sagte der Ritter und sah ein wenig verärgert aus. „So heißt der Name. Der Name ist wirklich "The Aged Aged Man". "

"Dann hätte ich sagen sollen 'So heißt das Lied'?" Alice korrigierte sich.

„Nein, das solltest du nicht: das ist eine ganz andere Sache! Das Lied heißt 'Ways And Means': aber so heißt es nur, weißt du! “

"Nun, was ist das Lied dann?" sagte Alice, die zu diesem Zeitpunkt völlig verwirrt war.

"Ich kam dazu", sagte der Ritter. "Das Lied ist wirklich 'A -itting On A Gate': und die Melodie ist meine eigene Erfindung."

Edd
quelle
1
Ich musste diese Passage ein paar Mal lesen ... +1, um mich zum Nachdenken zu bringen!
Ruben Steins
Deshalb ist Lewis Carroll kein gewöhnlicher Schriftsteller.
Metarose
1
Also ... würde es so gehen? Name -> 'The Aged Aged Man' -> genannt -> 'Haddock's Eyes' -> Lied -> 'A-Sitting On A Gate'
Tisaconundrum
12

Vielleicht möchten Sie dies lesen: Zeiger auf Zeiger

Ich hoffe, dies hilft, einige grundlegende Zweifel zu klären.

aJ.
quelle
7

Wenn ein Verweis auf einen Zeiger erforderlich ist. Zum Beispiel, wenn Sie den Wert (Adresse, auf die gezeigt wird) einer Zeigervariablen ändern möchten, die im Bereich einer aufrufenden Funktion innerhalb einer aufgerufenen Funktion deklariert ist.

Wenn Sie einen einzelnen Zeiger als Argument übergeben, ändern Sie lokale Kopien des Zeigers, nicht den ursprünglichen Zeiger im aufrufenden Bereich. Mit einem Zeiger auf einen Zeiger ändern Sie letzteren.

Alex Balashov
quelle
Gut erklärt für den "Warum" Teil
Rana Deep
7

Ein Zeiger auf einen Zeiger wird auch als Handle bezeichnet . Eine Verwendung dafür ist häufig, wenn ein Objekt im Speicher verschoben oder entfernt werden kann. Oft ist man dafür verantwortlich, die Nutzung des Objekts zu sperren und zu entsperren damit es beim Zugriff nicht verschoben wird.

Es wird häufig in Umgebungen mit eingeschränktem Speicher verwendet, z. B. unter Palm OS.

computer.howstuffworks.com Link >>

www.flippinbits.com Link >>

Epatel
quelle
7

Betrachten Sie die folgende Abbildung und das folgende Programm, um dieses Konzept besser zu verstehen .

Doppelzeigerdiagramm

Gemäß der Abbildung ist ptr1 ein einzelner Zeiger mit der Adresse der Variablen num .

ptr1 = #

In ähnlicher Weise ist ptr2 ein Zeiger auf einen Zeiger (Doppelzeiger), der die Adresse des Zeigers ptr1 hat .

ptr2 = &ptr1;

Ein Zeiger, der auf einen anderen Zeiger zeigt, wird als Doppelzeiger bezeichnet. In diesem Beispiel ist ptr2 ein Doppelzeiger.

Werte aus obigem Diagramm:

Address of variable num has : 1000
Address of Pointer ptr1 is: 2000
Address of Pointer ptr2 is: 3000

Beispiel:

#include <stdio.h>

int main ()
{
   int  num = 10;
   int  *ptr1;
   int  **ptr2;

   // Take the address of var 
   ptr1 = &num;

   // Take the address of ptr1 using address of operator &
   ptr2 = &ptr1;

   // Print the value
   printf("Value of num = %d\n", num );
   printf("Value available at *ptr1 = %d\n", *ptr1 );
   printf("Value available at **ptr2 = %d\n", **ptr2);
}

Ausgabe:

Value of num = 10
Value available at *ptr1 = 10
Value available at **ptr2 = 10
msc
quelle
5

Es ist ein Zeiger auf den Adresswert des Zeigers. (das ist schrecklich, ich weiß)

Grundsätzlich können Sie einen Zeiger auf den Wert der Adresse eines anderen Zeigers übergeben, sodass Sie ändern können, wohin ein anderer Zeiger von einer Unterfunktion zeigt, z.

void changeptr(int** pp)
{
  *pp=&someval;
}
Luke Schafer
quelle
Entschuldigung, ich weiß, dass es ziemlich schlimm war. Versuchen Sie, folgendes zu lesen: codeproject.com/KB/cpp/PtrToPtr.aspx
Luke Schafer
5

Sie haben eine Variable, die eine Adresse von etwas enthält. Das ist ein Hinweis.

Dann haben Sie eine andere Variable, die die Adresse der ersten Variablen enthält. Das ist ein Zeiger auf Zeiger.

Igor Oks
quelle
3

Ein Zeiger auf Zeiger ist ein Zeiger auf Zeiger.

Ein aussagekräftiges Beispiel für someType ** ist ein zweidimensionales Array: Sie haben ein Array, das mit Zeigern auf andere Arrays gefüllt ist, also beim Schreiben

dpointer [5] [6]

Sie greifen auf das Array zu, das Zeiger auf andere Arrays an seiner 5. Position enthält, erhalten den Zeiger (lassen Sie fpointer seinen Namen) und greifen dann auf das 6. Element des Arrays zu, auf das auf dieses Array verwiesen wird (also fpointer [6]).

Akappa
quelle
2
Zeiger auf Zeiger sollten nicht mit Arrays von Rang 2 verwechselt werden, z. B. int x [10] [10], wo Sie x [5] [6] schreiben, greifen Sie auf den Wert im Array zu.
Pete Kirkham
Dies ist nur ein Beispiel, bei dem eine Leere ** angemessen ist. Ein Zeiger auf Zeiger ist nur ein Zeiger, der auf einen Zeiger zeigt.
Akappa
1

So funktioniert es: Es ist eine Variable, die einen anderen Zeiger speichern kann.

Wann würden Sie sie verwenden: Viele verwenden eine davon, wenn Ihre Funktion ein Array erstellen und an den Aufrufer zurückgeben möchte.

//returns the array of roll nos {11, 12} through paramater
// return value is total number of  students
int fun( int **i )
{
    int *j;
    *i = (int*)malloc ( 2*sizeof(int) );
    **i = 11;  // e.g., newly allocated memory 0x2000 store 11
    j = *i;
    j++;
    *j = 12; ;  // e.g., newly allocated memory 0x2004 store 12

    return 2;
}

int main()
{
    int *i;
    int n = fun( &i ); // hey I don't know how many students are in your class please send all of their roll numbers.
    for ( int j=0; j<n; j++ )
        printf( "roll no = %d \n", i[j] );

    return 0;
}
Ergebnisweg
quelle
0

Es gibt so viele nützliche Erklärungen, aber ich habe nicht nur eine kurze Beschreibung gefunden, also ..

Grundsätzlich ist der Zeiger die Adresse der Variablen. Kurzer Zusammenfassungscode:

     int a, *p_a;//declaration of normal variable and int pointer variable
     a = 56;     //simply assign value
     p_a = &a;   //save address of "a" to pointer variable
     *p_a = 15;  //override the value of the variable

//print 0xfoo and 15 
//- first is address, 2nd is value stored at this address (that is called dereference)
     printf("pointer p_a is having value %d and targeting at variable value %d", p_a, *p_a); 

Nützliche Informationen finden Sie auch im Thema Was bedeutet Referenz und Dereferenzierung?

Und ich bin mir nicht so sicher, wann Zeiger nützlich sein können, aber gemeinsam ist es notwendig, sie zu verwenden, wenn Sie eine manuelle / dynamische Speicherzuweisung durchführen - Malloc, Calloc usw.

Ich hoffe also, dass es auch hilft, das Problem zu klären :)

xxxvodnikxxx
quelle