web-dev-qa-db-fra.com

Pourquoi les listes chaînées utilisent-elles des pointeurs au lieu de stocker des nœuds à l'intérieur de nœuds?

J'ai déjà beaucoup travaillé avec des listes de liens en Java, mais je suis très novice en C++. J'utilisais cette classe de nœud qui m'a été donnée dans un projet très bien

class Node
{
  public:
   Node(int data);

   int m_data;
   Node *m_next;
};

mais j'avais une question à laquelle on n'a pas très bien répondu. Pourquoi est-il nécessaire d'utiliser

Node *m_next;

pour pointer vers le noeud suivant dans la liste au lieu de

Node m_next;

Je comprends qu'il vaut mieux utiliser la version du pointeur. Je ne vais pas discuter des faits, mais je ne sais pas pourquoi c'est mieux. J'ai eu une réponse pas très claire sur la façon dont le pointeur est meilleur pour l'allocation de mémoire, et je me demandais si quelqu'un ici pourrait m'aider à mieux comprendre cela.

119
m0meni

Ce n'est pas simplement mieux, c'est le seul moyen possible.

Si vous stockiez un Nodeobjet à l'intérieur de lui-même, que serait sizeof(Node)? Ce serait sizeof(int) + sizeof(Node), qui serait égal à sizeof(int) + (sizeof(int) + sizeof(Node)), qui serait égal à sizeof(int) + (sizeof(int) + (sizeof(int) + sizeof(Node))), etc. à l'infini.

Un objet comme ça ne peut pas exister. C'est impossible.

217
emlai

En java

Node m_node

stocke un pointeur sur un autre noeud. Vous n'avez pas le choix à ce sujet. En C++

Node *m_node

signifie la même chose. La différence est qu'en C++, vous pouvez réellement stocker l'objet par opposition à un pointeur sur celui-ci. C'est pourquoi vous devez dire que vous voulez un pointeur. En C++:

Node m_node

signifie stocker le nœud ici (et que cela ne peut évidemment pas fonctionner pour une liste - vous vous retrouvez avec une structure définie de manière récursive).

178
pm100

C++ n'est pas Java. Quand tu écris

Node m_next;

en Java, cela revient à écrire

Node* m_next;

en C++. En Java, le pointeur est implicite, en C++, il est explicite. Si vous écrivez

Node m_next;

en C++, vous insérez une instance de Node à l'intérieur de l'objet que vous définissez. Il est toujours là et ne peut pas être omis, il ne peut pas être alloué avec new et il ne peut pas être supprimé. Cet effet est impossible à obtenir en Java et il est totalement différent de ce que Java fait avec la même syntaxe.

38
cmaster

Vous utilisez un pointeur, sinon votre code:

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

… Serait pas compiler, car le compilateur ne peut pas calculer la taille de Node. Cela est dû au fait que cela dépend de lui-même - ce qui signifie que le compilateur ne peut pas décider de la quantité de mémoire qu’il consommerait.

27
Nawaz

Le dernier (Node m_next) devrait contenir le nœud. Cela ne l'indiquerait pas. Et il n'y aurait alors aucun lien d'éléments.

13
wallyk

L'approche que vous décrivez est compatible non seulement avec C++, mais aussi avec son (principalement) sous-ensemble de langage C . Apprendre à développer une liste chaînée de style C est un bon moyen de vous familiariser avec les techniques de programmation de bas niveau (telles que la gestion manuelle de la mémoire), mais il n’est généralement pas pas une meilleure pratique pour le développement C++ moderne.

Ci-dessous, j'ai implémenté quatre variantes pour gérer une liste d'éléments en C++.

  1. raw_pointer_demo utilise la même approche que la vôtre - gestion manuelle de la mémoire requise avec l'utilisation de pointeurs bruts. L'utilisation de C++ ici ne concerne que syntactic-sugar, et l'approche utilisée est par ailleurs compatible avec le langage C.
  2. Dans shared_pointer_demo la gestion de la liste est toujours effectuée manuellement, mais la gestion de la mémoire est automatique (n’utilise pas de pointeurs bruts). Cela ressemble beaucoup à ce que vous avez probablement expérimenté avec Java.
  3. std_list_demo utilise la bibliothèque standard list . Cela montre à quel point les choses deviennent plus faciles si vous utilisez les bibliothèques existantes plutôt que de lancer la vôtre.
  4. std_vector_demo utilise la bibliothèque standard vector . Ceci gère le stockage de liste dans une seule allocation de mémoire contiguë. En d'autres termes, il n'y a pas de pointeur sur des éléments individuels. Dans certains cas assez extrêmes, cela peut devenir très inefficace. Cependant, dans des cas typiques, il s'agit de la meilleure pratique recommandée pour la gestion de liste en C++ .

À noter: de tous ceux-ci, seul le raw_pointer_demo nécessite en fait que la liste soit explicitement détruite afin d'éviter les "fuites" de mémoire. Les trois autres méthodes automatiquement détruiraient la liste et son contenu lorsque le conteneur serait hors de portée (à la fin de la fonction). Le point important étant que: C++ peut être très semblable à Java à cet égard, mais uniquement si vous choisissez de développer votre programme en utilisant les outils de haut niveau à votre disposition.


/*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();
}
9
nobar

Aperç

Il y a 2 façons de référencer et d'allouer des objets en C++, alors que dans Java, il n'y a qu'un seul moyen.

Pour expliquer cela, les schémas suivants montrent comment les objets sont stockés en mémoire.

1.1 Eléments C++ sans pointeurs

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       | |..
..| +-----------------------------+ |..
..+---------------------------------+..
.......................................

Warning: La syntaxe C++ utilisée dans cet exemple est similaire à la syntaxe Java. Mais l'allocation de mémoire est différente.

1.2 Eléments C++ utilisant des pointeurs

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 (...)

Si vous vérifiez la différence entre les deux manières, vous verrez que, dans la première technique, l'élément d'adresse est attribué au sein du client, tandis que dans la seconde, vous devez créer chaque adresse de manière explicite.

Attention: Java alloue des objets en mémoire de la même manière que cette seconde technique, mais la syntaxe est semblable à la première façon, ce qui peut prêter à confusion pour les nouveaux venus dans "C++".

Mise en oeuvre

Ainsi, votre exemple de liste pourrait être similaire à l'exemple suivant.

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].
.......................................

Résumé

Étant donné qu'une liste chaînée comporte une quantité variable d'éléments, la mémoire est allouée selon les besoins et selon les disponibilités.

MISE À JOUR:

Il convient également de mentionner, comme l'a commenté @haccks dans son message.

Parfois, les références ou les pointeurs d’objet indiquent des éléments imbriqués (par exemple, "Composition U.M.L.").

Et parfois, les références ou les pointeurs d’objets indiquent des éléments externes (par exemple, "U.M.L. Aggregation").

Toutefois, les éléments imbriqués de la même classe ne peuvent pas être appliqués avec la technique "sans pointeur".

8
umlcat

Sur une note de côté, si le tout premier membre d'une classe ou d'une structure est le pointeur suivant (donc, aucune fonction virtuelle ou toute autre fonctionnalité d'une classe pouvant signifier que next n'est pas le premier membre d'une classe ou d'une structure), alors vous pouvez utiliser une classe ou une structure "de base" avec juste un pointeur suivant, et utiliser un code commun pour les opérations de base d'une liste chaînée, telle que ajouter, insérer avant, récupérer de l'avant, .... En effet, C/C++ garantit que l'adresse du premier membre d'une classe ou d'une structure est identique à l'adresse de la classe ou de la structure. La classe ou la structure de nœud de base n'aurait qu'un pointeur suivant à utiliser par les fonctions de liste chaînées de base, puis la conversion de type serait utilisée selon les besoins pour convertir entre le type de nœud de base et les types de nœud "dérivés". Note secondaire - En C++, si la classe de nœud de base n'a qu'un pointeur suivant, je suppose que les classes dérivées ne peuvent pas avoir de fonctions virtuelles.

7
rcgldr

Pourquoi est-il préférable d'utiliser des pointeurs dans une liste chaînée?

La raison en est que lorsque vous créez un objet Node, le compilateur doit allouer de la mémoire pour cet objet et la taille de cet objet est calculée.
La taille du pointeur sur n'importe quel type est connue du compilateur et permet donc de calculer la taille du pointeur auto-référentiel.

Si Node m_node Est utilisé à la place, le compilateur n'a aucune idée de la taille de Node et il restera bloqué dans une récurrence infinie du calcul de sizeof(Node). Rappelez-vous toujours: une classe ne peut pas contenir un membre de son propre type .

6
haccks

Parce que cela dans C++

int main (..)
{
    MyClass myObject;

    // or

    MyClass * myObjectPointer = new MyClass();

    ..
}

est équivalent à ceci dans Java

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

où les deux créent un nouvel objet de MyClass en utilisant le constructeur par défaut.

5
Khaled.K