wake-up-neo.com

Welchen Nutzen hat ein Doppelzeiger?

Ich habe nach einer Antwort gesucht und gesucht, kann aber nichts finden, was ich tatsächlich "bekomme".

Ich bin sehr neu in C++ und kann mich nicht mit Doppel-, Dreifachzeigern usw. auseinandersetzen. Was ist der Sinn von ihnen?

Kann mich jemand aufklären? 

26
Mark Green

Ehrlich gesagt, sollten Sie in gut geschriebenem C++ sehr selten einen T** außerhalb des Bibliothekscodes sehen. Je mehr Sterne Sie haben, desto näher sind Sie am Gewinnen ein Preis einer bestimmten Art .

Das bedeutet nicht, dass ein Zeiger-zu-Zeiger niemals erforderlich ist. Möglicherweise müssen Sie einen Zeiger auf einen Zeiger aus demselben Grund erstellen, aus dem Sie jemals einen Zeiger auf einen anderen Objekttyp erstellen müssen.

Insbesondere könnte ich davon ausgehen, dass sich so etwas in einer Datenstruktur oder Algorithmusimplementierung befindet, wenn Sie dynamisch zugeteilte Knoten mischen, vielleicht?

Im Allgemeinen, außerhalb dieses Kontextes, wenn Sie einen Verweis auf einen Zeiger übergeben müssen, würden Sie genau das tun (z. B. T*&), anstatt sich auf Zeiger zu verdoppeln, und selbst das ist ziemlich selten .

Beim Stack Overflow werden Sie sehen, wie Menschen mit Zeigern auf Arrays von dynamisch zugewiesenen Zeigern auf Daten grässliche Dinge tun und versuchen, den am wenigsten effizienten "2D-Vektor" zu implementieren, den sie sich vorstellen können. Bitte lassen Sie sich nicht von ihnen inspirieren.

Zusammenfassend ist Ihre Intuition nicht unbegründet.

Ein wichtiger Grund, warum Sie über pointer-to-pointer -... wissen sollten/müssen sollten/müssen, ist, dass Sie manchmal über eine API (z. B. die Windows-API) mit anderen Sprachen (wie beispielsweise C) kommunizieren müssen.

Diese APIs verfügen häufig über Funktionen, die über einen Ausgabeparameter verfügen, der einen Zeiger zurückgibt. Diese anderen Sprachen verfügen jedoch häufig nicht über Referenzen oder kompatible (mit C++) Referenzen. Dies ist eine Situation, in der ein Zeiger-zu-Zeiger benötigt wird.

16
engf-010

Ich bin sehr, sehr neu in c ++ und kann mich nicht mit der Verwendung von Doppel-, Dreifachzeigern usw. auseinandersetzen. Was ist der Sinn von ihnen?

Der Trick, um Zeiger in C zu verstehen, besteht darin, einfach zu den Grundlagen zurückzukehren, die Ihnen wahrscheinlich nie beigebracht wurden. Sie sind:

  • Variablen speichern Werte eines bestimmten Typs.
  • Zeiger sind eine Art Wert.
  • Wenn x eine Variable vom Typ T ist, dann ist &x Ein Wert vom Typ T*.
  • Wenn x einen Wert vom Typ T* Ergibt, ist *x Eine Variable vom Typ T. Genauer...
  • ... wenn x einen Wert vom Typ T* ergibt, der für eine Variable a vom Typ T gleich &a ist, dann ist *x ein Alias ​​für a.

Nun folgt alles:

int x = 123;

x ist eine Variable vom Typ int. Sein Wert ist 123.

int* y = &x;

y ist eine Variable vom Typ int*. x ist eine Variable vom Typ int. &x Ist also ein Wert vom Typ int*. Daher können wir &x In y speichern.

*y = 456;

y ergibt den Inhalt der Variablen y. Das ist ein Wert vom Typ int*. Das Anwenden von * Auf einen Wert vom Typ int* Ergibt ein Variable vom Typ int. Deshalb können wir ihm 456 zuweisen. Was ist *y? Es ist ein Alias ​​für x. Deshalb haben wir x gerade 456 zugewiesen.

int** z = &y;

Was ist z? Es ist eine Variable vom Typ int**. Was ist &y? Da y eine Variable vom Typ int* Ist, muss &y Ein Wert vom Typ int** Sein. Deshalb können wir es z zuweisen.

**z = 789;

Was ist **z? Von innen nach außen arbeiten. z ergibt einen int**. Daher ist *z Eine Variable vom Typ int*. Es ist ein Alias ​​für y. Daher ist dies dasselbe wie *y Und wir wissen bereits, was das ist; Es ist ein Alias ​​für x.

Nein wirklich, worum geht es?

Hier habe ich ein Stück Papier. Es steht 1600 Pennsylvania Avenue Washington DC. Ist das ein Haus? Nein, es ist ein Stück Papier mit der Adresse eines Hauses. Aber wir können dieses Stück Papier benutzen, um das Haus zu finden .

Hier habe ich zehn Millionen Zettel, alle nummeriert. Die Papiernummer 123456 lautet 1600 Pennsylvania Avenue. Ist 123456 ein Haus? Ist es ein Stück Papier? Nein, aber es sind immer noch genug Informationen für mich, um das Haus zu finden .

Das ist der Punkt: Oft müssen wir uns aus Bequemlichkeitsgründen über mehrere Indirektionsebenen auf Entitäten beziehen .

Trotzdem sind doppelte Zeiger verwirrend und ein Zeichen dafür, dass Ihr Algorithmus nicht ausreichend abstrakt ist. Versuchen Sie, sie mit guten Designtechniken zu vermeiden.

12
Eric Lippert

Es wird weniger in C++ verwendet. In C kann es jedoch sehr nützlich sein. Angenommen, Sie haben eine Funktion, die eine zufällige Menge Speicherplatz mallokieren und den Speicher mit etwas füllen. Es wäre ein Schmerz, wenn Sie eine Funktion aufrufen müssen, um die Größe zu ermitteln, die Sie zuweisen müssen, und dann eine andere Funktion aufrufen, die den Speicher füllen wird. Stattdessen können Sie einen Doppelzeiger verwenden. Mit dem Doppelzeiger kann die Funktion den Zeiger auf den Speicherplatz setzen. Es gibt einige andere Dinge, für die es verwendet werden kann, aber das ist das Beste, was ich mir vorstellen kann.

int func(char** mem){
    *mem = malloc(50);
    return 50;
}

int main(){
    char* mem = NULL;
    int size = func(&mem);
    free(mem);
}
8
user3600107

Ein Doppelzeiger ist einfach ein Zeiger auf einen Zeiger. Eine übliche Verwendung ist für Arrays von Zeichenketten. Stellen Sie sich die erste Funktion in fast jedem C/C++ - Programm vor:

int main(int argc, char *argv[])
{
   ...
}

Was auch geschrieben werden kann

int main(int argc, char **argv)
{
   ...
}

Die Variable argv ist ein Zeiger auf ein Array von Zeigern auf Char. Dies ist eine Standardmethode, um Arrays von C "Strings" zu umgehen. Warum das tun? Ich habe gesehen, dass es für die Unterstützung mehrerer Sprachen, Blöcke von Fehlerzeichenfolgen usw. verwendet wird.

Vergessen Sie nicht, dass ein Zeiger nur eine Zahl ist - der Index des Speicherplatzes in einem Computer. Das ist es nicht mehr. Ein Doppelzeiger ist also der Index eines Speicherplatzes, der zufällig einen anderen Index an einen anderen Ort hält. Eine mathematische Verknüpfung der Punkte, wenn Sie möchten.

So erklärte ich meinen Kindern Hinweise:

Stellen Sie sich vor, der Computerspeicher besteht aus einer Reihe von Boxen. Auf jeder Box ist eine Zahl geschrieben, die bei Null beginnt und um 1 erhöht wird, so viele Bytes Speicher vorhanden sind. Angenommen, Sie haben einen Zeiger auf einen Speicherplatz. Dieser Zeiger ist nur die Boxnummer. Mein Zeiger ist zB 4. Ich schaue in Box 4. Drinnen ist noch eine Nummer, diesmal ist es 6. Also schauen wir uns nun Box 6 an und bekommen das letzte, was wir wollten. Mein ursprünglicher Zeiger (der "4" sagte) war ein Doppelzeiger, da der Inhalt seiner Box der Index einer anderen Box war und nicht das Endergebnis.

In letzter Zeit scheint es, als wären Zeiger selbst zu einem Paria der Programmierung geworden. In der nicht allzu fernen Vergangenheit war es völlig normal, Zeiger zu Zeigern umzuleiten. Mit der Verbreitung von Java und der zunehmenden Verwendung von Pass-by-Reference in C++ sank jedoch das grundlegende Verständnis von Zeigern - insbesondere, als sich Java als Anfängersprache für Informatik im ersten Jahr etablierte, sagen Pascal und C. 

Ich denke, eine Menge des Giftes über Zeiger besteht darin, dass die Leute sie einfach nie richtig verstehen. Dinge, die die Leute nicht verstehen, werden verspottet. Sie wurden also "zu hart" und "zu gefährlich". Ich vermute, selbst mit angeblich Gelehrten befürworten Smart Pointers usw. sind diese Ideen zu erwarten. Aber in Wirklichkeit gibt es ein sehr leistungsfähiges Programmiertool. Ehrlich gesagt, Zeiger sind die Magie des Programmierens, und schließlich sind sie nur eine Zahl.

5
Kingsley

In vielen Situationen ersetzt ein Foo*& einen Foo**. In beiden Fällen haben Sie einen Zeiger, dessen Adresse geändert werden kann.

Angenommen, Sie haben einen abstrakten Nicht-Wert-Typ und müssen diesen zurückgeben. Der Rückgabewert wird jedoch vom Fehlercode übernommen:

error_code get_foo( Foo** ppfoo )

oder

error_code get_foo( Foo*& pfoo_out )

Nun ist ein Funktionsargument, das veränderlich ist, selten nützlich, daher ist die Möglichkeit, den Ort desOutermostPointer ppFoo-Punkts zu ändern, selten nützlich. Ein Zeiger ist jedoch nullable -. Wenn das Argument von get_foo optional ist, fungiert ein Zeiger als optionale Referenz.

In diesem Fall ist der Rückgabewert ein Rohzeiger. Wenn eine eigene Ressource zurückgegeben wird, sollte dies normalerweise ein std::unique_ptr<Foo>* sein - ein intelligenter Zeiger auf dieser Ebene der Dereferenzierung.

Wenn stattdessen ein Zeiger auf etwas zurückgegeben wird, an dem er nicht beteiligt ist, ist ein Rohzeiger sinnvoller.

Neben diesen "groben Out-Parametern" gibt es noch weitere Verwendungen für Foo**. Wenn Sie einen polymorphen Nicht-Wert-Typ haben, sind die nicht-besitzenden Handles Foo* und derselbe Grund, warum Sie einen int* haben möchten, möchten Sie einen Foo**.

Was führt Sie dann zu der Frage "Warum wollen Sie einen int*?" In modernen C++ ist int* ein nicht veränderbarer, veränderbarer Verweis auf eine int. Es verhält sich besser, wenn es in einer struct gespeichert wird als eine Referenz (Verweise in Strukturen erzeugen verwirrende Semantik rund um Zuweisung und Kopie, insbesondere wenn sie mit Nichtverweisen gemischt werden).

Manchmal können Sie int* durch std::reference_wrapper<int>, gut std::optional<std::reference_wrapper<int>>, ersetzen. Beachten Sie jedoch, dass dies doppelt so groß sein wird wie ein einfacher int*.

Es gibt also legitime Gründe, int* zu verwenden. Sobald Sie das haben, können Sie Foo** legal verwenden, wenn Sie einen Zeiger auf einen Nicht-Wert-Typ möchten. Sie können sogar zu int** gelangen, indem Sie ein zusammenhängendes Array von int*s verwenden, mit dem Sie arbeiten möchten.

Rechtmäßig wird der Drei-Sterne-Programmierer immer schwieriger. Jetzt brauchen Sie einen legitimen Grund, um einen Foo** durch Indirektion zu übergeben. Normalerweise sollten Sie lange bevor Sie diesen Punkt erreichen, über die Zusammenfassung und/oder Vereinfachung Ihrer Codestruktur nachgedacht haben.

All dies ignoriert den häufigsten Grund. Interaktion mit C-APIs. C hat keinen unique_ptr, es hat keine span. Es werden tendenziell primitive Typen anstelle von Strukturen verwendet, da Strukturen einen auf Funktionen basierenden Zugriff erfordern (keine Überlastung des Operators).

Wenn also C++ mit C interagiert, erhalten Sie manchmal 0-3 mehr *s als der entsprechende C++ - Code.

Wenn Sie in C++ einen Zeiger als Out- oder In/Out-Parameter übergeben möchten, übergeben Sie ihn als Referenz:

int x;
void f(int *&p) { p = &x; }

Eine Referenz kann jedoch nicht ("legal") nullptr sein. Wenn der Zeiger optional ist, benötigen Sie einen Zeiger auf einen Zeiger:

void g(int **p) { if (p) *p = &x; }

Sicher, seit C++ 17 haben Sie std::optional, aber der "Doppelzeiger" ist seit Jahrzehnten idiomatischer C/C++ - Code, also sollte er in Ordnung sein. Auch die Nutzung ist nicht so schön, Sie auch:

void h(std::optional<int*> &p) { if (p) *p = &x) }

was auf der Anrufseite etwas hässlich ist, sofern Sie nicht bereits einen std::optional haben, oder:

void u(std::optional<std::reference_wrapper<int*>> p) { if (p) p->get() = &x; }

was an sich nicht so schön ist.

Einige mögen auch argumentieren, dass g auf der Anrufseite besser zu lesen ist:

f(p);
g(&p); // `&` indicates that `p` might change, to some folks
0

Die Verwendung besteht darin, einen Zeiger auf einen Zeiger zu haben, z. B. wenn Sie einen Zeiger per Referenz an eine Methode übergeben möchten.

0
Mureinik

Ein Fall, in dem ich es verwendet habe, ist eine Funktion, die eine verknüpfte Liste in C bearbeitet.

Es gibt

struct node { struct node *next; ... };

für die Listenknoten und

struct node *first;

auf das erste Element zeigen. Alle Manipulationsfunktionen benötigen einen struct node **, da ich garantieren kann, dass dieser Zeiger nicht -NULL ist, auch wenn die Liste leer ist, und ich brauche keine Sonderfälle für das Einfügen und Löschen.

void link(struct node *new_node, struct node **list)
{
    new_node->next = *list;
    *list = new_node;
}

void unlink(struct node **prev_ptr)
{
    *prev_ptr = (*prev_ptr)->next;
}

Um am Anfang der Liste einzufügen, übergeben Sie einfach einen Zeiger auf den first-Zeiger, und er wird das Richtige tun, auch wenn der Wert von firstNULL ist.

struct node *new_node = (struct node *)malloc(sizeof *new_node);
link(new_node, &first);
0
Simon Richter

Die mehrfache Indirektion ist größtenteils ein Überbleibsel von C (das weder Referenz- noch Containertypen hat). In gut geschriebenem C++ sollten Sie keine mehrfache Indirektion sehen, es sei denn, Sie haben es mit einer älteren C-Bibliothek oder ähnlichem zu tun.

Allerdings fällt die mehrfache Indirektion aus einigen ziemlich häufigen Anwendungsfällen heraus.

In C und C++ "zerfallen" Array-Ausdrücke in den meisten Fällen vom Typ "N-Element-Array von T" zu "Zeiger auf T"1. Nehmen wir also eine Array-Definition wie

T *a[N]; // for any type T

Wenn Sie a wie folgt an eine Funktion übergeben:

foo( a );

der Ausdrucka wird von "N-Element-Array von T *" in "Zeiger auf T *" oder T **, also was die Funktion tatsächlich empfängt, ist

void foo( T **a ) { ... }

Ein zweiter Punkt, den sie aufrufen, ist, wenn Sie möchten, dass eine Funktion einen Parameter vom Zeigertyp ändert, wie z

void foo( T **ptr )
{
  *ptr = new_value();
}

void bar( void )
{
  T *val;
  foo( &val );
}

Seitdem C++ Referenzen eingeführt hat, werden Sie das wahrscheinlich nicht mehr so ​​oft sehen. Das sehen Sie normalerweise nur, wenn Sie mit einer C-basierten API arbeiten.

Sie können auch mehrere Indirektionen verwenden, um "gezackte" Arrays einzurichten, aber Sie können dasselbe mit C++ - Containern für viel weniger Schmerz erreichen. Aber wenn Sie sich masochistisch fühlen:

T **arr;

try
{
  arr = new T *[rows];
  for ( size_t i = 0; i < rows; i++ )
    arr[i] = new T [size_for_row(i)];
}
catch ( std::bad_alloc& e )
{
  ...
}

In C++ sollten Sie jedoch meistens nur dann mehrere Indirektionen sehen, wenn ein Array von Zeigern auf einen Zeigerausdruck selbst "verfällt".




  1. Die Ausnahmen von dieser Regel treten auf, wenn der Ausdruck der Operand des Operators sizeof oder des unären Operators & Ist oder ein Zeichenfolgenliteral, das zum Initialisieren eines anderen Arrays in einer Deklaration verwendet wird.
0
John Bode

Welchen Nutzen hat ein Doppelzeiger?

Hier ist ein praktisches Beispiel. Angenommen, Sie haben eine Funktion und möchten ein Array von Zeichenfolgenparametern an sie senden (möglicherweise haben Sie eine DLL, an die Sie Parameter übergeben möchten). Das kann so aussehen:

#include <iostream>

void printParams(const char **params, int size)
{
    for (int i = 0; i < size; ++i)
    {
        std::cout << params[i] << std::endl;
    }
}

int main() 
{
    const char *params[] = { "param1", "param2", "param3" };
    printParams(params, 3);

    return 0;
}

Sie senden ein Array von const char-Zeigern, wobei jeder Zeiger auf den Anfang einer mit Null endenden C-Zeichenfolge zeigt. Der Compiler zerlegt Ihr Array in einen Zeiger bei einem Funktionsargument. Daher erhalten Sie const char ** einen Zeiger auf den ersten Zeiger des Arrays von const char-Zeigern. Da die Array-Größe zu diesem Zeitpunkt verloren geht, sollten Sie sie als zweites Argument übergeben.

0
Killzone Kid