Perché gli elenchi collegati utilizzano i puntatori invece di memorizzare i nodi all'interno dei nodi


121

Ho già lavorato molto con gli elenchi collegati in Java, ma sono molto nuovo in C ++. Stavo usando questa classe di nodi che mi è stata data in un progetto

class Node
{
  public:
   Node(int data);

   int m_data;
   Node *m_next;
};

ma avevo una domanda a cui non fu risposto molto bene. Perché è necessario utilizzare

Node *m_next;

per puntare al nodo successivo nell'elenco invece di

Node m_next;

Capisco che sia meglio usare la versione puntatore; Non ho intenzione di discutere i fatti, ma non so perché sia ​​meglio. Ho ottenuto una risposta non così chiara su come il puntatore è migliore per l'allocazione della memoria e mi chiedevo se qualcuno qui potesse aiutarmi a capirlo meglio.


14
@self Pardon me? Perché una lingua in cui tutto è un puntatore non dovrebbe avere elenchi collegati?
Angew non è più orgoglioso di SO

41
È importante notare come C e C ++ siano distinti da Java in termini di puntatori a oggetti e riferimenti. Node m_nextnon è un riferimento a un nodo, è l'archiviazione per l'intero Nodestesso.
Brian Cain

41
@self Java ha dei puntatori che semplicemente non li usi esplicitamente.
m0meni

27
TARTARUGHE fino in fondo è non è un'opzione. La follia deve finire da qualche parte.
WhozCraig

26
Per favore dimentica tutto quello che sai su Java. C ++ e Java gestiscono la memoria in modi fondamentalmente diversi. Vai a vedere questa domanda per i consigli sui libri, scegli uno e leggilo. Farai un enorme favore a tutti noi.
Rob K

Risposte:


218

Non è solo migliore, è l'unico modo possibile.

Se mettessi un Node oggetto al suo interno, quale sarebbe sizeof(Node)? Sarebbe sizeof(int) + sizeof(Node), che sarebbe uguale a sizeof(int) + (sizeof(int) + sizeof(Node)), che sarebbe uguale a sizeof(int) + (sizeof(int) + (sizeof(int) + sizeof(Node))), ecc. All'infinito.

Un oggetto del genere non può esistere. È impossibile .


25
* A meno che non venga valutato pigramente. Sono possibili elenchi infiniti, ma non con una valutazione rigorosa.
Carcigenicate

55
@Carcigenicate non si tratta di valutare / eseguire qualche funzione sull'oggetto Node - si tratta del layout di memoria di ogni istanza di Node, che deve essere determinata in fase di compilazione, prima che possa verificarsi qualsiasi valutazione.
Peteris

6
@DavidK È logicamente impossibile farlo. Hai bisogno di un puntatore (beh, davvero un riferimento indiretto) qui - certo che il linguaggio può nasconderlo, ma alla fine, niente da fare.
Voo

2
@David sono confuso. Prima sei d'accordo che è logicamente impossibile, ma poi vuoi contemplarlo? Rimuovi qualsiasi cosa di C o C ++: è impossibile in qualsiasi lingua che potresti mai immaginare per quanto posso vedere. Quella struttura è per definizione una ricorsione infinita e senza un certo livello di riferimento indiretto non possiamo romperla.
Voo

13
@benjamin ho effettivamente sottolineato (perché sapevo che altrimenti qualcuno lo avrebbe sollevato - beh, non ha aiutato) che Haskell ha allocato i thunk al momento della creazione e quindi questo funziona perché quei thunk ci danno l'indirizzamento di cui abbiamo bisogno. Questo non è altro che un puntatore con dati extra sotto mentite spoglie ...
Voo

178

In Java

Node m_node

memorizza un puntatore a un altro nodo. Non hai scelta al riguardo. In C ++

Node *m_node

significa la stessa cosa. La differenza è che in C ++ puoi effettivamente memorizzare l'oggetto invece di un puntatore ad esso. Ecco perché devi dire che vuoi un puntatore. In C ++:

Node m_node

significa memorizzare il nodo proprio qui (e questo chiaramente non può funzionare per un elenco: si finisce con una struttura definita ricorsivamente).


2
@SalmanA Lo sapevo già. Volevo solo sapere perché non avrebbe funzionato senza un puntatore, che è ciò che la risposta accettata spiegava molto meglio.
m0meni

3
@ AR7 Stanno entrambi dando la stessa spiegazione, solo con due approcci diversi. Se lo dichiarassi come una variabile "regolare", la prima volta che un costruttore veniva chiamato, lo istanzerebbe in una nuova istanza. Ma prima che finisca di istanziarlo - prima che il primo costruttore sia finito - Nodeverrebbe chiamato il costruttore del membro , che istanzerebbe un'altra nuova istanza ... e si otterrebbe una pseudo-ricorsione infinita. Non è davvero tanto un problema di dimensioni, in termini del tutto restrittivo e letterale, in quanto è un problema di prestazioni.
Panzercrisis

Ma tutto ciò che vuoi veramente è solo un modo per indicare il prossimo nell'elenco, non un Nodeche è effettivamente all'interno del primo Node. Quindi crei un puntatore, che è essenzialmente il modo in cui Java gestisce gli oggetti, al contrario delle primitive. Quando chiami un metodo o crei una variabile, Java non memorizza una copia dell'oggetto o nemmeno l'oggetto stesso; memorizza un riferimento a un oggetto, che è essenzialmente un puntatore con un piccolo guanto avvolto attorno ad esso. Questo è ciò che essenzialmente dicono entrambe le risposte.
Panzercrisis

non è un problema di dimensioni o velocità - è un problema di impossibilità. L'oggetto Node incluso includerebbe un oggetto Node che includerebbe un oggetto Node ... In effetti è impossibile compilarlo
pm100

3
@Panzercrisis sono consapevole che entrambi danno la stessa spiegazione. Questo approccio, tuttavia, non è stato altrettanto utile perché si concentrava su ciò di cui avevo già una comprensione: come funzionano i puntatori in C ++ e come vengono gestiti i puntatori in Java. La risposta accettata ha affrontato specificamente il motivo per cui non sarebbe stato possibile utilizzare un puntatore perché la dimensione non può essere calcolata. D'altra parte, questo lo lasciava più vagamente come "una struttura definita ricorsivamente". PS la tua spiegazione che hai appena scritto lo spiega meglio di entrambi: D.
m0meni

38

C ++ non è Java. Quando scrivi

Node m_next;

in Java, è come scrivere

Node* m_next;

in C ++. In Java, il puntatore è implicito, in C ++ è esplicito. Se scrivi

Node m_next;

in C ++, metti un'istanza di Nodeproprio lì all'interno dell'oggetto che stai definendo. È sempre presente e non può essere omesso, non può essere assegnato newe non può essere rimosso. Questo effetto è impossibile da ottenere in Java ed è totalmente diverso da ciò che fa Java con la stessa sintassi.


1
Per ottenere qualcosa di simile in Java sarebbe probabilmente "extends" se Supernodo estende il nodo, i Supernodi includono tutti gli attributi del nodo e devono riservare tutto lo spazio aggiuntivo. Quindi in Java non puoi fare "Node extends Node"
Falco

@Falco Vero, l'ereditarietà è una forma di inclusione sul posto delle classi base. Tuttavia, poiché Java non consente l'ereditarietà multipla (a differenza di C ++), è possibile eseguire il pull in un'istanza di una singola altra classe preesistente tramite l'ereditarietà. Ecco perché non penserei all'eredità come un sostituto dell'inclusione dei membri sul posto.
cmaster - ripristina monica il

27

Usi un puntatore, altrimenti il ​​tuo codice:

class Node
{
   //etc
   Node m_next; //non-pointer
};

... non si compilerebbe, poiché il compilatore non può calcolare la dimensione di Node. Questo perché dipende da se stesso, il che significa che il compilatore non può decidere quanta memoria consumare.


5
Peggio ancora, non esiste una dimensione valida: se k == sizeof(Node)contiene e il tuo tipo ha dati, dovrebbe anche contenere quello sizeof(Node) = k + sizeof(Data) = sizeof(Node) + sizeof(Data)e poi sizeof(Node) > sizeof(Node).
maschera di bit

4
@ maschera di bit non esiste una dimensione valida nei numeri reali . Se permetti i transinfiniti, aleph_0funziona. (Solo essere eccessivamente pedante :-))
k_g

2
@k_g Bene, lo standard C / C ++ impone che il valore restituito sizeofsia un tipo integrale senza segno, quindi c'è la speranza di dimensioni transfinite o addirittura reali. (essendo ancora più pedante!: p)
Thomas

@Thomas: Si potrebbe anche sottolineare che ci sono anche i numeri naturali. (Andando oltre il vertice pedantico: p)
maschera di bit

1
In effetti, Nodenon è nemmeno definito prima della fine di questo snippet, quindi non puoi davvero usarlo all'interno. Consentire a uno di dichiarare implicitamente in avanti puntatori a una classe non ancora dichiarata è un piccolo trucco consentito dal linguaggio per rendere possibili tali strutture, senza la necessità di lanciare esplicitamente puntatori tutto il tempo.
osa

13

Quest'ultimo ( Node m_next) dovrebbe contenere il nodo. Non lo indicherebbe. E quindi non ci sarebbe alcun collegamento di elementi.


3
Peggio ancora, sarebbe logicamente impossibile per un oggetto contenere qualcosa dello stesso tipo.
Mike Seymour

Non ci sarebbe ancora tecnicamente il collegamento perché sarebbe un nodo contenente un nodo contenente un nodo e così via?
m0meni

9
@ AR7: No, contenimento significa che è letteralmente all'interno dell'oggetto, non collegato ad esso.
Mike Seymour

9

L'approccio che si descrive è compatibile non solo con C ++, ma anche con la sua (per lo più) lingua sottoinsieme C . Imparare a sviluppare un elenco collegato in stile C è un buon modo per presentarsi alle tecniche di programmazione di basso livello (come la gestione manuale della memoria), ma generalmente non è una best practice per lo sviluppo C ++ moderno.

Di seguito, ho implementato quattro varianti su come gestire un elenco di elementi in C ++.

  1. raw_pointer_demoutilizza lo stesso approccio del tuo: gestione manuale della memoria richiesta con l'uso di puntatori non elaborati. L'uso del C ++ qui è solo per lo zucchero sintattico e l'approccio utilizzato è altrimenti compatibile con il linguaggio C.
  2. Nella shared_pointer_demolista la gestione è ancora fatta manualmente, ma la gestione della memoria è automatica (non usa puntatori raw). Questo è molto simile a quello che probabilmente hai sperimentato con Java.
  3. std_list_demoutilizza il listcontenitore della libreria standard . Questo mostra quanto le cose diventino più facili se ti affidi a librerie esistenti piuttosto che a creare le tue.
  4. std_vector_demoutilizza il vectorcontenitore della libreria standard . Questo gestisce l'archiviazione dell'elenco in una singola allocazione di memoria contigua. In altre parole, non ci sono puntatori a singoli elementi. Per alcuni casi piuttosto estremi, questo può diventare notevolmente inefficiente. Per i casi tipici, tuttavia, questa è la procedura consigliata per la gestione degli elenchi in C ++ .

Da notare: di tutti questi, solo in raw_pointer_demorealtà richiede che l'elenco venga esplicitamente distrutto per evitare "perdite" di memoria. Gli altri tre metodi distruggerebbero automaticamente l'elenco e il suo contenuto quando il contenitore esce dall'ambito (alla conclusione della funzione). Il punto è: il C ++ è in grado di essere molto "simile a Java" in questo senso - ma solo se scegli di sviluppare il tuo programma utilizzando gli strumenti di alto livello a tua disposizione.


/*BINFMTCXX: -Wall -Werror -std=c++11
*/

#include <iostream>
#include <algorithm>
#include <string>
#include <list>
#include <vector>
#include <memory>
using std::cerr;

/** Brief   Create a list, show it, then destroy it */
void raw_pointer_demo()
{
    cerr << "\n" << "raw_pointer_demo()..." << "\n";

    struct Node
    {
        Node(int data, Node *next) : data(data), next(next) {}
        int data;
        Node *next;
    };

    Node * items = 0;
    items = new Node(1,items);
    items = new Node(7,items);
    items = new Node(3,items);
    items = new Node(9,items);

    for (Node *i = items; i != 0; i = i->next)
        cerr << (i==items?"":", ") << i->data;
    cerr << "\n";

    // Erase the entire list
    while (items) {
        Node *temp = items;
        items = items->next;
        delete temp;
    }
}

raw_pointer_demo()...
9, 3, 7, 1

/** Brief   Create a list, show it, then destroy it */
void shared_pointer_demo()
{
    cerr << "\n" << "shared_pointer_demo()..." << "\n";

    struct Node; // Forward declaration of 'Node' required for typedef
    typedef std::shared_ptr<Node> Node_reference;

    struct Node
    {
        Node(int data, std::shared_ptr<Node> next ) : data(data), next(next) {}
        int data;
        Node_reference next;
    };

    Node_reference items = 0;
    items.reset( new Node(1,items) );
    items.reset( new Node(7,items) );
    items.reset( new Node(3,items) );
    items.reset( new Node(9,items) );

    for (Node_reference i = items; i != 0; i = i->next)
        cerr << (i==items?"":", ") << i->data;
    cerr<<"\n";

    // Erase the entire list
    while (items)
        items = items->next;
}

shared_pointer_demo()...
9, 3, 7, 1

/** Brief   Show the contents of a standard container */
template< typename C >
void show(std::string const & msg, C const & container)
{
    cerr << msg;
    bool first = true;
    for ( int i : container )
        cerr << (first?" ":", ") << i, first = false;
    cerr<<"\n";
}

/** Brief  Create a list, manipulate it, then destroy it */
void std_list_demo()
{
    cerr << "\n" << "std_list_demo()..." << "\n";

    // Initial list of integers
    std::list<int> items = { 9, 3, 7, 1 };
    show( "A: ", items );

    // Insert '8' before '3'
    items.insert(std::find( items.begin(), items.end(), 3), 8);
    show("B: ", items);

    // Sort the list
    items.sort();
    show( "C: ", items);

    // Erase '7'
    items.erase(std::find(items.begin(), items.end(), 7));
    show("D: ", items);

    // Erase the entire list
    items.clear();
    show("E: ", items);
}

std_list_demo()...
A:  9, 3, 7, 1
B:  9, 8, 3, 7, 1
C:  1, 3, 7, 8, 9
D:  1, 3, 8, 9
E:

/** brief  Create a list, manipulate it, then destroy it */
void std_vector_demo()
{
    cerr << "\n" << "std_vector_demo()..." << "\n";

    // Initial list of integers
    std::vector<int> items = { 9, 3, 7, 1 };
    show( "A: ", items );

    // Insert '8' before '3'
    items.insert(std::find(items.begin(), items.end(), 3), 8);
    show( "B: ", items );

    // Sort the list
    sort(items.begin(), items.end());
    show("C: ", items);

    // Erase '7'
    items.erase( std::find( items.begin(), items.end(), 7 ) );
    show("D: ", items);

    // Erase the entire list
    items.clear();
    show("E: ", items);
}

std_vector_demo()...
A:  9, 3, 7, 1
B:  9, 8, 3, 7, 1
C:  1, 3, 7, 8, 9
D:  1, 3, 8, 9
E:

int main()
{
    raw_pointer_demo();
    shared_pointer_demo();
    std_list_demo();
    std_vector_demo();
}

La Node_referencedichiarazione di cui sopra affronta una delle differenze a livello di linguaggio più interessanti tra Java e C ++. In Java, la dichiarazione di un oggetto di tipo Nodeutilizza implicitamente un riferimento a un oggetto allocato separatamente. In C ++, puoi scegliere tra allocazione di riferimento (puntatore) e diretta (stack), quindi devi gestire la distinzione in modo esplicito. Nella maggior parte dei casi useresti l'allocazione diretta, sebbene non per gli elementi dell'elenco.
Brent Bradburn

Non so perché non ho consigliato anche la possibilità di uno std :: deque .
Brent Bradburn

8

Panoramica

Ci sono 2 modi per fare riferimento e allocare oggetti in C ++, mentre in Java c'è solo un modo.

Per spiegare questo, i seguenti diagrammi mostrano come gli oggetti vengono archiviati in memoria.

1.1 Elementi C ++ senza puntatori

class AddressClass
{
  public:
    int      Code;
    char[50] Street;
    char[10] Number;
    char[50] POBox;
    char[50] City;
    char[50] State;
    char[50] Country;
};

class CustomerClass
{
  public:
    int          Code;
    char[50]     FirstName;
    char[50]     LastName;
    // "Address" IS NOT A pointer !!!
    AddressClass Address;
};

int main(...)
{
   CustomerClass MyCustomer();
     MyCustomer.Code = 1;
     strcpy(MyCustomer.FirstName, "John");
     strcpy(MyCustomer.LastName, "Doe");
     MyCustomer.Address.Code = 2;
     strcpy(MyCustomer.Address.Street, "Blue River");
     strcpy(MyCustomer.Address.Number, "2231 A");

   return 0;
} // int main (...)

.......................................
..+---------------------------------+..
..|          AddressClass           |..
..+---------------------------------+..
..| [+] int:      Code              |..
..| [+] char[50]: Street            |..
..| [+] char[10]: Number            |..
..| [+] char[50]: POBox             |..
..| [+] char[50]: City              |..
..| [+] char[50]: State             |..
..| [+] char[50]: Country           |..
..+---------------------------------+..
.......................................
..+---------------------------------+..
..|          CustomerClass          |..
..+---------------------------------+..
..| [+] int:      Code              |..
..| [+] char[50]: FirstName         |..
..| [+] char[50]: LastName          |..
..+---------------------------------+..
..| [+] AddressClass: Address       |..
..| +-----------------------------+ |..
..| | [+] int:      Code          | |..
..| | [+] char[50]: Street        | |..
..| | [+] char[10]: Number        | |..
..| | [+] char[50]: POBox         | |..
..| | [+] char[50]: City          | |..
..| | [+] char[50]: State         | |..
..| | [+] char[50]: Country       | |..
..| +-----------------------------+ |..
..+---------------------------------+..
.......................................

Avvertenza : la sintassi C ++ utilizzata in questo esempio è simile alla sintassi in Java. Ma l'allocazione della memoria è diversa.

1.2 Elementi C ++ che utilizzano i puntatori

class AddressClass
{
  public:
    int      Code;
    char[50] Street;
    char[10] Number;
    char[50] POBox;
    char[50] City;
    char[50] State;
    char[50] Country;
};

class CustomerClass
{
  public:
    int           Code;
    char[50]      FirstName;
    char[50]      LastName;
    // "Address" IS A pointer !!!
    AddressClass* Address;
};

.......................................
..+-----------------------------+......
..|        AddressClass         +<--+..
..+-----------------------------+...|..
..| [+] int:      Code          |...|..
..| [+] char[50]: Street        |...|..
..| [+] char[10]: Number        |...|..
..| [+] char[50]: POBox         |...|..
..| [+] char[50]: City          |...|..
..| [+] char[50]: State         |...|..
..| [+] char[50]: Country       |...|..
..+-----------------------------+...|..
....................................|..
..+-----------------------------+...|..
..|         CustomerClass       |...|..
..+-----------------------------+...|..
..| [+] int:      Code          |...|..
..| [+] char[50]: FirstName     |...|..
..| [+] char[50]: LastName      |...|..
..| [+] AddressClass*: Address  +---+..
..+-----------------------------+......
.......................................

int main(...)
{
   CustomerClass* MyCustomer = new CustomerClass();
     MyCustomer->Code = 1;
     strcpy(MyCustomer->FirstName, "John");
     strcpy(MyCustomer->LastName, "Doe");

     AddressClass* MyCustomer->Address = new AddressClass();
     MyCustomer->Address->Code = 2;
     strcpy(MyCustomer->Address->Street, "Blue River");
     strcpy(MyCustomer->Address->Number, "2231 A");

     free MyCustomer->Address();
     free MyCustomer();

   return 0;
} // int main (...)

Se controlli la differenza tra le due modalità, vedrai che nella prima tecnica l'elemento indirizzo è allocato all'interno del cliente, mentre nella seconda modalità devi creare esplicitamente ciascun indirizzo.

Attenzione: Java alloca gli oggetti in memoria come questa seconda tecnica, ma la sintassi è come il primo modo, il che potrebbe creare confusione per i nuovi arrivati ​​in "C ++".

Implementazione

Quindi il tuo esempio di elenco potrebbe essere qualcosa di simile al seguente esempio.

class Node
{
  public:
   Node(int data);

   int m_data;
   Node *m_next;
};

.......................................
..+-----------------------------+......
..|            Node             |......
..+-----------------------------+......
..| [+] int:           m_data   |......
..| [+] Node*:         m_next   +---+..
..+-----------------------------+...|..
....................................|..
..+-----------------------------+...|..
..|            Node             +<--+..
..+-----------------------------+......
..| [+] int:           m_data   |......
..| [+] Node*:         m_next   +---+..
..+-----------------------------+...|..
....................................|..
..+-----------------------------+...|..
..|            Node             +<--+..
..+-----------------------------+......
..| [+] int:           m_data   |......
..| [+] Node*:         m_next   +---+..
..+-----------------------------+...|..
....................................v..
...................................[X].
.......................................

Sommario

Poiché un elenco collegato ha una quantità variabile di elementi, la memoria viene allocata in base alle esigenze e, in base alla disponibilità.

AGGIORNARE:

Vale anche la pena menzionare, come ha commentato @haccks nel suo post.

A volte, riferimenti o puntatori a oggetti, indicano elementi nidificati (alias "Composizione UML").

E a volte, riferimenti o puntatori a oggetti, indicano elementi esterni (noti anche come "aggregazione UML").

Tuttavia, elementi annidati della stessa classe, non possono essere applicati con la tecnica "no-pointer".


7

In una nota a margine, se il primo membro di una classe o di una struttura è il puntatore successivo (quindi nessuna funzione virtuale o qualsiasi altra caratteristica di una classe che significherebbe che successivo non è il primo membro di una classe o di una struttura), allora tu può usare una classe o una struttura "base" con solo un puntatore successivo, e usare codice comune per operazioni di base sulle liste concatenate come aggiungere, inserire prima, recuperare da davanti, .... Questo perché C / C ++ garantisce che l'indirizzo del primo membro di una classe o di una struttura sia lo stesso dell'indirizzo della classe o della struttura. La classe o la struttura del nodo di base avrebbe solo un puntatore successivo da utilizzare dalle funzioni di base dell'elenco collegato, quindi il typecasting verrebbe utilizzato come necessario per la conversione tra il tipo di nodo di base e i tipi di nodo "derivati". Nota a margine: in C ++, se la classe del nodo base ha solo un puntatore successivo,


6

Perché è meglio utilizzare i puntatori in un elenco collegato?

Il motivo è che quando crei un Nodeoggetto, il compilatore deve allocare memoria per quell'oggetto e per questo viene calcolata la dimensione dell'oggetto.
La dimensione del puntatore a qualsiasi tipo è nota al compilatore e quindi con il puntatore autoreferenziale è possibile calcolare la dimensione dell'oggetto.

Se Node m_nodeviene utilizzato invece, il compilatore non ha idea della dimensione di Nodee si bloccherà in una ricorsione infinita del calcolo sizeof(Node). Ricorda sempre: una classe non può contenere un membro del proprio tipo .


5

Perché questo in C ++

int main (..)
{
    MyClass myObject;

    // or

    MyClass * myObjectPointer = new MyClass();

    ..
}

è equivalente a questo in Java

public static void main (..)
{
    MyClass myObjectReference = new MyClass();
}

dove entrambi creano un nuovo oggetto MyClassutilizzando il costruttore predefinito.


0

Perché gli elenchi collegati utilizzano puntatori invece di memorizzare nodi all'interno dei nodi?

Ovviamente c'è una risposta banale.

Se non collegassero un nodo al successivo tramite un puntatore, non sarebbero elenchi collegati .

L'esistenza di elenchi concatenati come una cosa è perché vogliamo essere in grado di concatenare oggetti insieme. Ad esempio: abbiamo già un oggetto da qualche parte. Ora vogliamo mettere quell'oggetto reale (non una copia) alla fine di una coda, per esempio. Ciò si ottiene aggiungendo un collegamento dall'ultimo elemento già in coda alla voce che stiamo aggiungendo. In termini di macchina, questo significa riempire una parola con l'indirizzo dell'elemento successivo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.