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.
Ce n'est pas simplement mieux, c'est le seul moyen possible.
Si vous stockiez un Node
objet à 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.
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).
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.
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.
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.
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++.
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.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.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.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();
}
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".
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.
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 .
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.