Wie funktionieren Pointer-to-Pointer in C? (und wann könnten Sie sie verwenden?)

Lesezeit: 12 Minuten

Wie funktionieren Pointer-to-Pointer in C?
Wann könnten Sie sie verwenden?

  • Nein, keine Hausaufgaben … wollte nur wissen … weil ich es oft sehe, wenn ich C-Code lese.

    Debugger

    22. Mai 2009 um 11:18 Uhr

  • Ein Zeiger auf einen Zeiger ist kein Sonderfall von etwas, daher verstehe ich nicht, was Sie an void** nicht verstehen.

    – Akappa

    22. Mai 2009 um 11:19 Uhr

  • für 2D-Arrays ist das beste Beispiel die Befehlszeilen-Argumente “prog arg1 arg2”, die in char**argv gespeichert sind. Und wenn der Aufrufer den Speicher nicht zuweisen möchte (die aufgerufene Funktion wird den Speicher zuweisen)

    – Ergebnisweg

    21. März 2013 um 22:29 Uhr


  • Sie haben ein schönes Beispiel für die Verwendung von “Zeiger zu Zeiger” in Git 2.0: siehe meine Antwort unten

    – VonC

    12. März 2014 um 14:40 Uhr

Benutzeravatar von Stephan202
Stefan202

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 „hello“ beginnt. Wenn dies also in diesem Fall das einzige Vorkommen von „Hallo“ im Speicher ist,

const char *c = "hello";

… definiert c ein Zeiger auf den (schreibgeschützten) String “hello” sein und somit den Wert 63 enthält. c selbst irgendwo gespeichert werden: im obigen Beispiel an Stelle 58. Natürlich können wir nicht nur auf Zeichen zeigen, sondern auch auf andere Zeiger. Z.B:

const char **cp = &c;

Jetzt cp verweist auf cdas heißt, es enthält die Adresse von c (das ist 58). Wir können sogar noch weiter gehen. In Betracht ziehen:

const char ***cpp = &cp;

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


Bezüglich warum man verwendet Zeiger auf Zeiger:

  • Der Name eines Arrays ergibt normalerweise die Adresse seines ersten Elements. Wenn das Array also Elemente vom Typ enthält tein Verweis auf das Array hat den Typ t *. Stellen Sie sich nun ein Array von Arrays des Typs vor t: Natürlich hat ein Verweis auf dieses 2D-Array einen Typ (t *)* = t **und ist daher ein Zeiger auf einen Zeiger.
  • Auch wenn ein Array von Strings eindimensional klingt, ist es tatsächlich zweidimensional, da Strings Zeichen-Arrays sind. Somit: char **.
  • Eine Funktion f muss ein Argument vom Typ akzeptieren t ** wenn es eine Variable des Typs ändern soll t *.
  • Viele andere Gründe, die zu zahlreich sind, um sie hier aufzulisten.

  • Ja, gutes Beispiel … ich verstehe, was sie sind … aber wie und wann sie verwendet werden, ist wichtiger … jetzt …

    Debugger

    22. Mai 2009 um 11:33 Uhr

  • Stephan hat gute Arbeit geleistet, indem er im Grunde das Diagramm in The C Programming Language von Kernighan & Richie reproduziert hat. Wenn Sie C programmieren und dieses Buch nicht haben und mit Papierdokumentation zufrieden sind, empfehle ich Ihnen dringend, es zu besorgen, die (ziemlich) bescheidenen Kosten werden sich sehr schnell in der Produktivität bezahlt machen. Es neigt dazu, in seinen Beispielen sehr klar zu sein.

    – J. Pölfer

    22. Mai 2009 um 13:33 Uhr

  • char* c = “Hallo” sollte const char* c = “Hallo” sein. Auch ist es höchstens irreführend zu sagen, dass “ein Array als Adresse des ersten Elements gespeichert wird”. Ein Array wird gespeichert als … ein Array. Oft liefert 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 an den Zeiger).

    – Bastien Léonard

    22. Mai 2009 um 20:12 Uhr

  • Wenn ich diese Antwort nicht falsch interpretiere, sieht sie falsch aus. c ist bei 58 gespeichert und zeigt auf 63, cp ist bei 55 gespeichert und zeigt auf 58, und cpp ist in dem Diagramm nicht dargestellt.

    – Thanatos

    23. Mai 2009 um 16:16 Uhr

  • Sieht gut aus. Alles, was mich davon abgehalten hat, zu sagen, war nur ein kleines Problem: Toller Beitrag. Die Erklärung selbst war ausgezeichnet. Wechsel zu einer Upvote. (Vielleicht muss Stackoverflow Zeiger überprüfen?)

    – Thanatos

    23. Mai 2009 um 16:54 Uhr

Benutzeravatar von Brian R. Bondy
Brian R. Bondy

Wie funktionieren Zeiger auf Zeiger in C?

Erstens ist ein Zeiger eine Variable, wie jede andere Variable, die aber die Adresse einer Variablen enthält.

Ein Zeiger auf einen Zeiger ist eine Variable, wie jede andere Variable, die aber die Adresse einer Variablen enthält. Diese Variable ist einfach 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 Verwendungen, wie zum Beispiel das Argument main() jedes C-Programms hat einen Zeiger auf einen Zeiger für argv, wobei jedes Element ein Array von Zeichen enthält, die die Befehlszeilenoptionen sind. 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 korrekt ausgeführten Zeiger auf ein zweidimensionales Array:

int (*myPointerTo2DimArray)[ROWS][COLUMNS]

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

Benutzeravatar von VonC
VonC

Ich mag dieses “reale” Codebeispiel der Zeiger-zu-Zeiger-Verwendung in Git 2.0, übertrage 7b1004b:

Linus sagte einmal:

Ich wünschte tatsächlich, dass mehr Leute die wirklich grundlegende Art der Codierung auf niedriger Ebene verstehen würden. Kein großes, komplexes Zeug wie die Namenssuche ohne Sperre, sondern einfach eine gute Verwendung von Zeigern auf Zeiger usw.
Zum Beispiel habe ich zu viele Leute gesehen, die einen einfach verknüpften Listeneintrag löschen, indem sie den „prev“-Eintrag verfolgen und dann den Eintrag löschen, indem sie so etwas tun:


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

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

Leute, die Zeiger verstehen, verwenden einfach ein “Zeiger auf den Eintragszeiger“, und initialisieren Sie diese mit der Adresse von list_head. Und dann, wenn sie die Liste durchlaufen, können sie den Eintrag ohne Bedingung entfernen, indem sie einfach a ausführen

*pp =  entry->next

Zeiger

Wenn wir diese Vereinfachung anwenden, verlieren wir 7 Zeilen 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 Video von 2016 darauf hin “Das Doppelzeigerproblem von Linus Torvalds“.


kumar weist in den Kommentaren auf den Blogbeitrag “Linus über das Verständnis von Zeigern“, wo Grisha Trubetskoy erklärt:

Stellen Sie sich vor, Sie haben eine verknüpfte Liste, die wie folgt definiert ist:

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

Sie müssen es von Anfang bis Ende durchlaufen und ein bestimmtes Element entfernen, dessen Wert dem Wert von to_remove entspricht.
Der offensichtlichere 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:

  • Iterieren über die Liste, bis der Eintrag ist NULLwas 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,
  • wodurch das aktuelle Element eliminiert wird (Zeile 7).

Oben gibt es einen Sonderfall – zu Beginn der Iteration gibt es keinen vorherigen Eintrag (prev ist NULL), und um den ersten Eintrag in der Liste zu entfernen, müssen Sie head selbst ändern (Zeile 9).

Das hat Linus gesagt Der obige Code könnte vereinfacht werden, 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;
       else
            pp = &entry->next;
       entry = entry->next;
   }

Der obige Code ist der vorherigen Variante sehr ähnlich, aber beachten Sie, dass wir seitdem nicht mehr auf den Sonderfall des ersten Elements der Liste achten müssen pp ist nicht NULL am Anfang. Einfach und clever.

Außerdem hat jemand in diesem Thread kommentiert, dass der Grund dafür ist, dass dies besser ist, weil *pp = entry->next ist atomar. 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 ziemlich nichts in C jemals als atomar angesehen werden (einschließlich der ++ und -- Betreiber)!

  • Dies wird helfen, besser zu verstehen – grisha.org/blog/2013/04/02/linus-on-understanding-pointers

    – Kumar

    12. Mai 2014 um 15:52 Uhr


  • @kumar gute Referenz. Ich habe es für mehr Sichtbarkeit in die Antwort aufgenommen.

    – VonC

    12. Mai 2014 um 17:04 Uhr

  • Dieses Video war für mich wesentlich, um Ihr Beispiel zu verstehen. Insbesondere fühlte ich mich verwirrt (und aggressiv), bis ich ein Gedächtnisdiagramm zeichnete und den Fortschritt des Programms verfolgte. Trotzdem erscheint es mir immer noch etwas mysteriös.

    – Chris

    11. Juli 2017 um 10:15 Uhr

  • @Chris Tolles Video, danke für die Erwähnung! Ich habe Ihren Kommentar zur besseren Sichtbarkeit in die Antwort aufgenommen.

    – VonC

    11. Juli 2017 um 17:09 Uhr

  • @VonC Dieser Code wird nicht funktionieren wenn es zwei oder mehr Einträge gibt, die value(s) gleich sind to_remove. Denn nach dem Entfernen des ersten pp = &entry->next; wird ausgeführt, wenn es nicht sollte. Aufgrund dieses Fehlers zeigt pp auf den Speicherplatz des nächsten Zeigers des gerade gelöschten Knotens und anschließend löschen, *pp = entry->next; schreibt die Adresse des Elements, das dem gelöschten Knoten folgt (entry->next), zum nächsten Zeiger des zuvor gelöschten Knotens, der den durchtrennt Link(e) in verlinkter Liste.

    – Abetancort

    25. September 2020 um 11:08 Uhr


Benutzeravatar von Edd
Edd

Als wir Hinweise auf einen Programmierkurs an der Universität behandelten, bekamen wir zwei Hinweise, wie wir damit anfangen sollten. Das erste war zu sehen Pointer-Spaß mit Binky. Die zweite war, über die nachzudenken Schellfischaugen Passage von Lewis Carrolls Genauer betrachtet

„Du bist traurig“, sagte der Ritter in besorgtem Ton, „lass mich dir ein Lied singen, um dich zu trösten.“

„Ist es sehr lang?“ fragte Alice, denn sie hatte an diesem Tag eine Menge Gedichte gehört.

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

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

»Sonst tut es das nicht, wissen Sie. Der Name des Songs heißt ‚Haddocks‘ Eyes‘.“

„Oh, das ist der Name des Liedes, oder?“, sagte Alice und versuchte interessiert zu sein.

„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 etwas ganz anderes! Der Song heißt ‚Ways And Means‘, aber er heißt nur so, weißt du!“

„Nun, was ist denn das Lied?“ sagte Alice, die zu diesem Zeitpunkt völlig verwirrt war.

»Dazu wollte ich gerade kommen«, sagte der Ritter. „Der Song ist wirklich ‚A-sitting On A Gate‘: und die Melodie ist meine eigene Erfindung.“

Benutzeravatar von aJ
aJ.

Zeiger auf Zeiger

Da wir Zeiger auf int und Zeiger auf char und Zeiger auf beliebige Strukturen haben können, die wir definiert haben, und tatsächlich Zeiger auf jeden Typ in C, sollte es nicht allzu überraschend sein, dass wir Zeiger darauf haben können andere Hinweise.

Benutzeravatar von msc
msc

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

Doppelzeiger-Diagramm

Gemäß der Abbildung, ptr1 ist ein einzelner Zeiger die Adresse der Variablen hat Anzahl.

ptr1 = #

Ähnlich ptr2 ist ein Zeiger auf Zeiger (Doppelzeiger) die die Adresse des Zeigers hat ptr1.

ptr2 = &ptr1;

Ein Zeiger, der auf einen anderen Zeiger zeigt, wird als Doppelzeiger bezeichnet. In diesem Beispiel ptr2 ist 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

Benutzeravatar von Rob Bednark
Rob Bednark

Ein Zeiger-auf-einen-Zeiger wird verwendet, wenn ein Verweis auf einen Zeiger erforderlich ist. Wenn Sie beispielsweise den Wert (die 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 Gültigkeitsbereich. Mit einem Zeiger auf einen Zeiger modifizieren Sie letzteren.

  • Gut erklärt für den ‘Warum’-Teil

    – Ranatief

    16. Februar 2014 um 9:53 Uhr

1425700cookie-checkWie funktionieren Pointer-to-Pointer in C? (und wann könnten Sie sie verwenden?)

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy