Articles

Pointeurs C++ : pourquoi nous en avons besoin, quand nous les utilisons, comment ils diffèrent de l’accès à l’objet lui-même

Même si la plupart des programmeurs comprennent quelle est la différence entre les objets et les pointeurs vers eux, parfois il n’est pas tout à fait clair, laquelle des manières d’accéder à l’objet choisir. Nous avons essayé de répondre à cette question ci-dessous.

Question

Je viens d’un milieu Java et j’ai commencé à travailler avec des objets en C++. Mais une chose qui m’est apparue est que les gens utilisent souvent des pointeurs vers des objets plutôt que les objets eux-mêmes, par exemple cette déclaration:

Object *myObject = new Object;

plutôt que:

Object myObject;

Ou au lieu d’utiliser une fonction, disons testFunc(), comme ceci:

myObject.testFunc();

nous devons écrire:

myObject->testFunc();

Mais je n’arrive pas à comprendre pourquoi devrions-nous le faire de cette façon. Je suppose que cela a à voir avec l’efficacité et la vitesse puisque nous obtenons un accès direct à l’adresse de la mémoire. Ai-je raison ?

Réponse

A propos, nous devons noter, que les pointeurs en Java ne sont pas utilisés explicitement, par exemple, un programmeur ne peut pas accéder à un objet dans le code à travers un pointeur sur celui-ci. Cependant, en Java, tous les types, à l’exception de la base, sont référencés : l’accès à ceux-ci se fait par le lien, bien que vous ne puissiez pas explicitement passer le paramètre par le lien. A part cela, nouveau en C++ et Java ou C# sont des choses différentes.

Afin de donner une légère idée des pointeurs en C++ , nous allons donner deux fragments de code similaires:

Java:

Object object1 = new Object(); //A new object is allocated by JavaObject object2 = new Object(); //Another new object is allocated by Javaobject1 = object2; //object1 now points to the object originally allocated for object2//The object originally allocated for object1 is now "dead" – //nothing points to it, so it//will be reclaimed by the Garbage Collector.//If either object1 or object2 is changed, //the change will be reflected to the other

L’équivalent le plus proche de ceci, est :

C++:

Object * object1 = new Object(); //A new object is allocated on the heapObject * object2 = new Object(); //Another new object is allocated on the heapdelete object1;//Since C++ does not have a garbage collector, //if we don't do that, the next line would //cause a "memory leak", i.e. a piece of claimed memory that //the app cannot use //and that we have no way to reclaim...object1 = object2; //Same as Java, object1 points to object2.

Voyons l’alternative C++:

Object object1; //A new object is allocated on the STACKObject object2; //Another new object is allocated on the STACKobject1 = object2;//!!!! This is different! //The CONTENTS of object2 are COPIED onto object1,//using the "copy assignment operator", the definition of operator =.//But, the two objects are still different. //Change one, the other remains unchanged.//Also, the objects get automatically destroyed //once the function returns...

Est-ce que nous obtenons un gain de vitesse, en accédant directement à la mémoire ?

En fait, pas du tout. Les pointeurs sont généralement utilisés pour l’accès au tas tandis que les objets sont situés dans la pile – c’est une structure plus simple et plus rapide. Si vous êtes un débutant, nous avons pour vous du matériel dans lequel nous disons en détail ce qu’est une pile et un tas.

Strictement parlant, cette question combine deux problèmes différents. Premièrement : quand utilisons-nous l’allocation dynamique de la mémoire ? Deuxièmement : quand est-il préférable d’utiliser des pointeurs ? Bien sûr, nous ne ferons pas sans les mots communs que vous devez toujours choisir l’outil le plus approprié pour le travail. Presque toujours, il y a une meilleure réalisation que d’utiliser l’allocation dynamique manuelle (allocation dynamique) et/ou les pointeurs bruts.

Il est très regrettable que vous voyiez l’allocation dynamique si souvent. Cela montre simplement combien de mauvais programmeurs C++ il y a.

Dans un sens, vous avez deux questions regroupées en une seule. La première est quand devrions-nous utiliser l’allocation dynamique (en utilisant new) ? La seconde est quand devrions-nous utiliser les pointeurs?

Le message important à retenir est que vous devez toujours utiliser l’outil approprié pour le travail. Dans presque toutes les situations, il existe quelque chose de plus approprié et de plus sûr que d’effectuer une allocation dynamique manuelle et/ou d’utiliser des pointeurs bruts.

Allocation dynamique

Dans votre question, vous avez démontré deux façons de créer un objet. La principale différence est la durée de stockage de l’objet. Lorsque vous faites Object myObject ; dans un bloc, l’objet est créé avec une durée de stockage automatique, ce qui signifie qu’il sera détruit automatiquement lorsqu’il sortira de la portée. Lorsque vous faites new Object(), l’objet a une durée de stockage dynamique, ce qui signifie qu’il reste en vie jusqu’à ce que vous le supprimiez explicitement. Vous ne devez utiliser la durée de stockage dynamique que lorsque vous en avez besoin. C’est-à-dire que vous devriez toujours préférer créer des objets avec une durée de stockage automatique quand vous le pouvez.

Les deux principales situations dans lesquelles vous pourriez avoir besoin d’une allocation dynamique :

  1. Vous avez besoin que l’objet survive à la portée actuelle – cet objet spécifique à cet emplacement mémoire spécifique, pas une copie de celui-ci. Si vous êtes d’accord pour copier/déplacer l’objet (la plupart du temps, vous devriez l’être), vous devriez préférer un objet automatique.
  2. Vous avez besoin d’allouer beaucoup de mémoire, ce qui peut facilement remplir la pile. Ce serait bien si nous n’avions pas à nous préoccuper de cela (la plupart du temps, vous ne devriez pas avoir à le faire), car c’est vraiment en dehors du domaine du C++, mais malheureusement nous devons faire face à la réalité des systèmes pour lesquels nous développons.
  3. Vous ne connaissez pas exactement la taille du tableau, que vous devrez utiliser. Comme vous le savez, en C++ ont la taille des tableaux est fixe. Cela peut poser des problèmes, par exemple, lors de la lecture des entrées utilisateur. Le pointeur définit seulement cette section de la mémoire, où le début d’un tableau sera écrit, ne limitant pas sa taille.

Si une utilisation de l’allocation dynamique est nécessaire, vous devriez l’encapsuler en utilisant un pointeur intelligent ou d’un autre type qui supporte l’idiome « L’acquisition de la ressource est l’initialisation » (les conteneurs standard le supportent – c’est un idiome, conformément à laquelle la ressource : un bloc de mémoire, fichier, connexion réseau, etc – sont initialisés tout en obtenant dans le constructeur, puis soigneusement sont détruits par destructeur). Par exemple, les pointeurs intelligents sont std::unique_ptr et std::shared_ptr

Pointers

Cependant, il existe d’autres utilisations plus générales des pointeurs bruts au-delà de l’allocation dynamique, mais la plupart ont des alternatives que vous devriez préférer. Comme précédemment, préférez toujours les alternatives à moins que vous n’ayez vraiment besoin de pointeurs.

  1. Vous avez besoin de la sémantique des références. Parfois, vous voulez passer un objet en utilisant un pointeur (indépendamment de la façon dont il a été alloué) parce que vous voulez que la fonction à laquelle vous le passez ait accès à cet objet spécifique (pas une copie de celui-ci). Cependant, dans la plupart des situations, vous devriez préférer les types de référence aux pointeurs, car c’est précisément pour cela qu’ils ont été conçus. Notez qu’il ne s’agit pas nécessairement d’étendre la durée de vie de l’objet au-delà de la portée actuelle, comme dans la situation 1 ci-dessus. Comme avant, si vous êtes d’accord pour passer une copie de l’objet, vous n’avez pas besoin de sémantique de référence.
  2. Vous avez besoin de polymorphisme. Vous ne pouvez appeler les fonctions de manière polymorphe (c’est-à-dire en fonction du type dynamique d’un objet) qu’à travers un pointeur ou une référence à l’objet. Si c’est le comportement dont vous avez besoin, alors vous devez utiliser des pointeurs ou des références. Encore une fois, les références devraient être préférées.
  3. Vous voulez représenter qu’un objet est facultatif en permettant à un nullptr d’être passé lorsque l’objet est omis. Si c’est un argument, vous devriez préférer utiliser des arguments par défaut ou des surcharges de fonctions. Sinon, vous devriez préférer utiliser un type qui encapsule ce comportement, comme std::optional (introduit en C++17 – avec les normes C++ antérieures, utilisez boost::optional).
  4. Vous voulez découpler les unités de compilation pour améliorer le temps de compilation. La propriété utile d’un pointeur est que vous n’avez besoin que d’une déclaration directe du type pointé (pour utiliser réellement l’objet, vous aurez besoin d’une définition). Cela vous permet de découpler certaines parties de votre processus de compilation, ce qui peut améliorer considérablement le temps de compilation. Voir l’idiome Pimpl.
  5. Vous avez besoin de vous interfacer avec une bibliothèque C ou une bibliothèque de style C. À ce stade, vous êtes obligé d’utiliser des pointeurs bruts. La meilleure chose que vous pouvez faire est de vous assurer que vous ne lâchez vos pointeurs bruts qu’au dernier moment possible. Vous pouvez obtenir un pointeur brut à partir d’un pointeur intelligent, par exemple, en utilisant sa fonction membre get. Si une bibliothèque effectue une allocation pour vous qu’elle s’attend à ce que vous désallouiez via un handle, vous pouvez souvent envelopper le handle dans un smart pointer avec un deleter personnalisé qui désallouera l’objet de manière appropriée.