1733 votes

Pourquoi devrais-je utiliser un pointeur plutôt que l'objet lui-même ?

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 on devrait le faire de cette façon. Je suppose que c'est pour des raisons d'efficacité et de rapidité, puisque nous avons un accès direct à l'adresse de la mémoire. N'ai-je pas raison ?

141 votes

Si vous ne voyez pas de raison d'utiliser des pointeurs, ne le faites pas. Préférez les objets. Préférez les objets à l'unique_ptr, au shared_ptr et aux pointeurs bruts.

126 votes

Note : en java, tout (sauf les types de base) est un pointeur. vous devriez donc plutôt vous demander l'inverse : pourquoi ai-je besoin d'objets simples ?

129 votes

Notez que, en Java, les pointeurs sont cachés par la syntaxe. En C++, la différence entre un pointeur et un non-pointeur est explicitée dans le code. Java utilise les pointeurs partout.

1690voto

Joseph Mansfield Points 59346

C'est très malheureux de voir l'allocation dynamique si souvent. Cela montre simplement combien de mauvais programmeurs C++ il y a.

En un sens, vous avez deux questions regroupées en une seule. La première est de savoir quand nous devons utiliser l'allocation dynamique (en utilisant new ) ? La deuxième question est la suivante : quand faut-il 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 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; à l'intérieur d'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 sa portée. Lorsque vous faites new Object() l'objet a une durée de stockage dynamique, ce qui veut dire qu'il reste en vie jusqu'à ce que vous ayez explicitement mis en place une procédure d'enregistrement. delete il. Vous ne devez utiliser la durée de stockage dynamique que lorsque vous en avez besoin. C'est-à-dire, vous devriez siempre préférez créer des objets avec une durée de stockage automatique lorsque vous pouvez .

Les deux principales situations dans lesquelles vous pouvez avoir besoin d'une allocation dynamique :

  1. Il faut que l'objet survive à la portée actuelle. - cet objet spécifique à cet emplacement mémoire spécifique, et non 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 devez 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 ce n'est vraiment pas du ressort du C++, mais malheureusement, nous devons faire face à la réalité des systèmes pour lesquels nous développons.

Lorsque vous avez absolument besoin d'une allocation dynamique, vous devez l'encapsuler dans un pointeur intelligent ou un autre type qui effectue RAII (comme les conteneurs standard). Les pointeurs intelligents fournissent la sémantique de la propriété des objets alloués dynamiquement. Jetez un coup d'œil à std::unique_ptr y std::shared_ptr par exemple. Si vous les utilisez de manière appropriée, vous pouvez presque entièrement éviter d'effectuer votre propre gestion de la mémoire (voir la section Règle du zéro ).

Pointeurs

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 avant, toujours préférer les alternatives, sauf si vous avez vraiment besoin de pointeurs .

  1. Vous avez besoin d'une sémantique de référence . Parfois, vous souhaitez transmettre un objet à l'aide d'un pointeur (indépendamment de la manière dont il a été alloué) parce que vous voulez que la fonction à laquelle vous le transmettez ait accès à cet objet spécifique (et non à 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 précédemment, si vous êtes d'accord pour passer une copie de l'objet, vous n'avez pas besoin de la sémantique de référence.

  2. Vous avez besoin du 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. Là encore, les références sont à privilégier.

  3. Vous voulez représenter qu'un objet est facultatif en permettant à un nullptr à passer 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, il est préférable d'utiliser un type qui encapsule ce comportement, tel que std::optional (introduit en C++17 - avec les normes C++ antérieures, utiliser 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 le idiome Pimpl .

  5. Vous devez 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 puissiez faire est de vous assurer que vous ne lâchez vos pointeurs bruts qu'au dernier moment. Vous pouvez obtenir un pointeur brut à partir d'un pointeur intelligent, par exemple, en utilisant sa fonction get fonction membre. Si une bibliothèque effectue une allocation pour vous et qu'elle s'attend à ce que vous la 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.

88 votes

"Vous avez besoin que l'objet survive à la portée actuelle". -- Une remarque supplémentaire à ce sujet : il y a des cas où il semble que vous ayez besoin que l'objet survive à la portée actuelle, mais ce n'est pas vraiment le cas. Si vous placez votre objet à l'intérieur d'un vecteur, par exemple, l'objet sera copié (ou déplacé) dans le vecteur, et l'objet original pourra être détruit en toute sécurité à la fin de sa portée.

0 votes

"Vous avez besoin que l'objet survive à la portée actuelle" - On peut aussi retourner l'objet, si possible. Bien que ce soit une copie de l'objet qui soit retournée, le code est assez soigné. A moins que la copie ne soit lourde, j'ai tendance à retourner l'objet (si possible et si c'est logique).

25 votes

Rappelez-vous que s/copy/move/ dans de nombreux endroits maintenant. Le fait de retourner un objet n'implique absolument pas un déplacement. Vous devez également noter que l'accès à un objet par le biais d'un pointeur est orthogonal à la façon dont il a été créé.

182voto

TemplateRex Points 26447

Les pointeurs sont utilisés dans de nombreux cas.

Comportement polymorphe . Pour les types polymorphes, les pointeurs (ou références) sont utilisés pour éviter le découpage en tranches :

class Base { ... };
class Derived : public Base { ... };

void fun(Base b) { ... }
void gun(Base* b) { ... }
void hun(Base& b) { ... }

Derived d;
fun(d);    // oops, all Derived parts silently "sliced" off
gun(&d);   // OK, a Derived object IS-A Base object
hun(d);    // also OK, reference also doesn't slice

Sémantique des références et prévention de la copie . Pour les types non polymorphes, un pointeur (ou une référence) évitera de copier un objet potentiellement coûteux.

Base b;
fun(b);  // copies b, potentially expensive 
gun(&b); // takes a pointer to b, no copying
hun(b);  // regular syntax, behaves as a pointer

Notez que C++11 a une sémantique de déplacement qui peut éviter de nombreuses copies d'objets coûteux en argument de fonction et en valeur de retour. Mais l'utilisation d'un pointeur permet d'éviter ces copies et autorise l'utilisation de plusieurs pointeurs sur le même objet (alors qu'un objet ne peut être déplacé qu'une seule fois).

Acquisition de ressources . Création d'un pointeur vers une ressource à l'aide de la fonction new est un opérateur anti-modèle en C++ moderne. Utilisez une classe de ressource spéciale (l'un des conteneurs standard) ou un conteneur pointeur intelligent ( std::unique_ptr<> o std::shared_ptr<> ). Considérez :

{
    auto b = new Base;
    ...       // oops, if an exception is thrown, destructor not called!
    delete b;
}

vs.

{
    auto b = std::make_unique<Base>();
    ...       // OK, now exception safe
}

Un pointeur brut ne doit être utilisé que comme "vue" et ne doit en aucun cas être impliqué dans la propriété, que ce soit par la création directe ou implicitement par les valeurs de retour. Voir aussi cette question-réponse tirée de la FAQ C++ .

Un contrôle plus fin de la durée de vie Chaque fois qu'un pointeur partagé est copié (par exemple, en tant qu'argument de fonction), la ressource vers laquelle il pointe est maintenue en vie. Les objets ordinaires (non créés par new (que ce soit directement par vous ou à l'intérieur d'une classe de ressources) sont détruits lorsqu'ils sortent du champ d'application.

18 votes

"Créer un pointeur vers une ressource en utilisant l'opérateur new est un anti-modèle" Je pense que vous pourriez même améliorer cela pour avoir un pointeur brut propre à quelque chose est un anti-modèle . Non seulement la création, mais le passage de pointeurs bruts en tant qu'arguments ou valeurs de retour impliquant un transfert de propriété est, à mon avis, déprécié depuis que l'on a mis en place le système de gestion de la propriété. unique_ptr /sémantique du déplacement

6 votes

Utiliser des pointeurs intelligents partout est un anti-modèle. Il existe quelques cas particuliers où elle est applicable, mais la plupart du temps, les mêmes raisons qui plaident en faveur de l'allocation dynamique (durée de vie arbitraire) plaident également contre les pointeurs intelligents habituels.

2 votes

@JamesKanze Je ne voulais pas dire que les pointeurs intelligents devraient être utilisés partout, juste pour la propriété, et aussi que les pointeurs bruts ne devraient pas être utilisés pour la propriété, mais seulement pour les vues.

137voto

Gerasimos R Points 478

Il y a beaucoup d'excellentes réponses à cette question, y compris les cas d'utilisation importants des déclarations prospectives, du polymorphisme, etc. mais j'ai l'impression qu'une partie de l'"âme" de votre question n'est pas répondue - à savoir ce que signifient les différentes syntaxes entre Java et C++.

Examinons la situation en comparant les deux langues :

Java :

Object object1 = new Object(); //A new object is allocated by Java
Object object2 = new Object(); //Another new object is allocated by Java

object1 = 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 heap
Object * object2 = new Object(); //Another new object is allocated on the heap
delete 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 la méthode alternative C++ :

Object object1; //A new object is allocated on the STACK
Object object2; //Another new object is allocated on the STACK
object1 = 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...

La meilleure façon d'y penser est que -- plus ou moins -- Java gère (implicitement) les pointeurs vers les objets, tandis que le C++ peut gérer soit les pointeurs vers les objets, soit les objets eux-mêmes. Il y a des exceptions à cela -- par exemple, si vous déclarez des types Java "primitifs", ce sont des valeurs réelles qui sont copiées, et non des pointeurs. Ainsi,

Java :

int object1; //An integer is allocated on the stack.
int object2; //Another integer is allocated on the stack.
object1 = object2; //The value of object2 is copied to object1.

Cela dit, l'utilisation de pointeurs n'est PAS nécessairement la bonne ou la mauvaise façon de gérer les choses ; cependant, d'autres réponses ont couvert ce point de façon satisfaisante. L'idée générale est qu'en C++ vous avez beaucoup plus de contrôle sur la durée de vie des objets, et sur l'endroit où ils vont vivre.

Point de départ : le Object * object = new Object() est en fait ce qui se rapproche le plus de la sémantique typique de Java (ou de C#, d'ailleurs).

2 votes

Object object1 = new Object(); Object object2 = new Object(); est un très mauvais code. Le second new ou le second constructeur d'objet peut être rejeté, et maintenant object1 est perdu. Si vous utilisez des new vous devez envelopper new dans les wrappers RAII dès que possible.

10 votes

En effet, ce serait le cas si c'était un programme et que rien d'autre ne se passait autour. Heureusement, il ne s'agit que d'un extrait d'explication montrant comment se comporte un pointeur en C++ - et l'un des rares endroits où un objet RAII ne peut être substitué à un pointeur brut, c'est l'étude et l'apprentissage des pointeurs bruts...

85voto

user3391320 Points 388

Préface

Java n'a rien à voir avec C++, contrairement à ce qui est dit. La machine à fabriquer du Java voudrait vous faire croire que parce que Java a une syntaxe similaire à celle du C++, les deux langages sont similaires. Rien n'est plus éloigné de la vérité. Cette désinformation est en partie la raison pour laquelle les programmeurs Java se tournent vers le C++ et utilisent une syntaxe similaire à celle de Java sans comprendre les implications de leur code.

En avant, nous allons

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

Au contraire, en fait. Le tas est beaucoup plus lent que la pile, car la pile est très simple par rapport au tas. Les variables de stockage automatique (aka variables de pile) ont leurs destructeurs appelés une fois qu'elles sortent de la portée. Par exemple :

{
    std::string s;
}
// s is destroyed here

En revanche, si vous utilisez un pointeur alloué dynamiquement, son destructeur doit être appelé manuellement. delete appelle ce destructeur pour vous.

{
    std::string* s = new std::string;
}
delete s; // destructor called

Cela n'a rien à voir avec le new syntaxe répandue en C# et Java. Elles sont utilisées à des fins complètement différentes.

Avantages de l'allocation dynamique

1. Il n'est pas nécessaire de connaître à l'avance la taille du tableau.

L'un des premiers problèmes rencontrés par de nombreux programmeurs C++ est que, lorsqu'ils acceptent des entrées arbitraires de la part des utilisateurs, vous ne pouvez allouer qu'une taille fixe à une variable de pile. Vous ne pouvez pas non plus modifier la taille des tableaux. Par exemple :

char buffer[100];
std::cin >> buffer;
// bad input = buffer overflow

Bien sûr, si vous avez utilisé un std::string à la place, std::string se redimensionne en interne, ce qui ne devrait pas poser de problème. Mais la solution à ce problème est essentiellement l'allocation dynamique. Vous pouvez allouer de la mémoire dynamique en fonction de l'entrée de l'utilisateur, par exemple :

int * pointer;
std::cout << "How many items do you need?";
std::cin >> n;
pointer = new int[n];

Note complémentaire : Une erreur que font beaucoup de débutants est l'utilisation de tableaux de longueur variable. Il s'agit d'une extension de GNU et également de Clang parce qu'ils reflètent de nombreuses extensions de GCC. Ainsi, le tableau suivant int arr[n] ne doit pas être invoquée.

Comme le tas est beaucoup plus grand que la pile, on peut arbitrairement allouer/réallouer autant de mémoire que nécessaire, alors que la pile a une limite.

2. Les tableaux ne sont pas des pointeurs

En quoi est-ce un avantage, me demandez-vous ? La réponse deviendra claire une fois que vous aurez compris la confusion/le mythe derrière les tableaux et les pointeurs. On pense généralement qu'ils sont identiques, mais ce n'est pas le cas. Ce mythe vient du fait que les pointeurs peuvent être souscrits tout comme les tableaux et que les tableaux se transforment en pointeurs au niveau le plus élevé de la déclaration d'une fonction. Cependant, une fois qu'un tableau se transforme en pointeur, le pointeur perd son statut d'objet. sizeof informations. Ainsi, sizeof(pointer) donnera la taille du pointeur en octets, qui est généralement de 8 octets sur un système 64 bits.

Vous ne pouvez pas assigner aux tableaux, seulement les initialiser. Par exemple :

int arr[5] = {1, 2, 3, 4, 5}; // initialization 
int arr[] = {1, 2, 3, 4, 5}; // The standard dictates that the size of the array
                             // be given by the amount of members in the initializer  
arr = { 1, 2, 3, 4, 5 }; // ERROR

D'autre part, vous pouvez faire tout ce que vous voulez avec les pointeurs. Malheureusement, comme la distinction entre les pointeurs et les tableaux est présentée de façon très souple en Java et en C#, les débutants ne comprennent pas la différence.

3. Polymorphisme

Java et C# disposent de facilités qui vous permettent de traiter les objets comme un autre, par exemple en utilisant la fonction as mot-clé. Donc si quelqu'un voulait traiter un Entity en tant qu'objet Player on pourrait faire Player player = Entity as Player; Ceci est très utile si vous avez l'intention d'appeler des fonctions sur un conteneur homogène qui ne doivent s'appliquer qu'à un type spécifique. Cette fonctionnalité peut être réalisée de manière similaire ci-dessous :

std::vector<Base*> vector;
vector.push_back(&square);
vector.push_back(&triangle);
for (auto& e : vector)
{
     auto test = dynamic_cast<Triangle*>(e); // I only care about triangles
     if (!test) // not a triangle
        e.GenericFunction();
     else
        e.TriangleOnlyMagic();
}

Ainsi, si seuls les Triangles disposent d'une fonction Rotate, une erreur de compilation se produirait si vous essayiez de l'appeler sur tous les objets de la classe. Utilisation de dynamic_cast vous pouvez simuler le as mot-clé. Pour être clair, si un cast échoue, il renvoie un pointeur invalide. Donc !test est essentiellement un raccourci pour vérifier si test est NULL ou un pointeur invalide, ce qui signifie que le transfert a échoué.

Avantages des variables automatiques

Après avoir vu toutes les possibilités offertes par l'allocation dynamique, vous vous demandez probablement pourquoi personne n'utiliserait l'allocation dynamique tout le temps ? Je vous ai déjà donné une raison : le tas est lent. Et si vous n'avez pas besoin de toute cette mémoire, vous ne devriez pas en abuser. Voici donc quelques inconvénients, sans ordre particulier :

  • Elle est source d'erreurs. L'allocation manuelle de mémoire est dangereuse et vous êtes sujet à des fuites. Si vous ne maîtrisez pas l'utilisation du débogueur ou de la fonction valgrind (un outil de fuite de mémoire), vous risquez de vous arracher les cheveux. Heureusement, les idiomes RAII et les pointeurs intelligents atténuent un peu ce problème, mais vous devez être familier avec des pratiques telles que la règle de trois et la règle de cinq. Cela fait beaucoup d'informations à assimiler, et les débutants qui ne savent pas ou qui ne s'en soucient pas tomberont dans ce piège.

  • Ce n'est pas nécessaire. Contrairement à Java et C# où il est idiomatique d'utiliser l'attribut new partout, en C++, vous ne devez l'utiliser que si vous en avez besoin. L'expression courante dit que tout ressemble à un clou si vous avez un marteau. Alors que les débutants en C++ sont effrayés par les pointeurs et apprennent à utiliser les variables de pile par habitude, les programmeurs Java et C# commencer en utilisant des pointeurs sans les comprendre ! C'est littéralement partir du mauvais pied. Vous devez abandonner tout ce que vous savez, car la syntaxe est une chose, l'apprentissage du langage en est une autre.

1. (N)RVO - Alias, Optimisation de la valeur de retour (nommée)

Une optimisation que beaucoup de compilateurs font sont des choses appelées élision y optimisation de la valeur de retour . Ces éléments peuvent éviter des copies inutiles, ce qui est utile pour les objets de très grande taille, comme un vecteur contenant de nombreux éléments. Normalement, la pratique courante est d'utiliser des pointeurs sur transfert de propriété plutôt que de copier les gros objets dans déplacer les entourer. Cela a conduit à la création de sémantique du déplacement y pointeurs intelligents .

Si vous utilisez des pointeurs, (N)RVO fait NO se produire. Il est plus avantageux et moins sujet aux erreurs de tirer parti de la (N)RVO plutôt que de renvoyer ou de passer des pointeurs si l'optimisation vous préoccupe. Des fuites d'erreurs peuvent se produire si l'appelant d'une fonction est chargé de delete d'un objet alloué dynamiquement, etc. Il peut être difficile de suivre la propriété d'un objet si les pointeurs sont transmis comme une patate chaude. Utilisez simplement les variables de pile car c'est plus simple et plus efficace.

0 votes

"Donc !test est essentiellement un raccourci pour vérifier si test est NULL ou un pointeur invalide, ce qui signifie que le cast a échoué." Je pense que cette phrase doit être réécrite pour plus de clarté.

4 votes

"La machine à hype Java voudrait vous faire croire" -- peut-être en 1997, mais c'est maintenant anachronique, il n'y a plus de motivation pour comparer Java à C++ en 2014.

17 votes

Vieille question, mais dans le segment de code { std::string* s = new std::string; } delete s; // destructor called ....surely this delete ne fonctionnera pas parce que le compilateur ne saura pas ce que les s l'est encore ?

80voto

Burnt Toast Points 611

Une autre bonne raison d'utiliser des pointeurs serait pour déclarations préalables . Dans un projet suffisamment important, ils peuvent vraiment accélérer le temps de compilation.

3 votes

Std::shared_ptr<T> fonctionne également avec les déclarations forward de T. (std::unique_ptr<T> n'a pas )

13 votes

@berkus : std::unique_ptr<T> fonctionne avec les déclarations préalables de T . Vous devez simplement vous assurer que lorsque le destructeur de l'objet std::unique_ptr<T> s'appelle, T est un type complet. Cela signifie généralement que votre classe qui contient le std::unique_ptr<T> déclare son destructeur dans le fichier d'en-tête et l'implémente dans le fichier cpp (même si l'implémentation est vide).

0 votes

Les modules vont-ils résoudre ce problème ?

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X