web-dev-qa-db-fra.com

Quand devrais-je utiliser le nouveau mot-clé en C ++?

J'utilise C++ depuis peu de temps et je m'interroge sur le mot-clé nouvea. Devrais-je simplement l'utiliser, ou pas?

1) Avec le nouvea mot clé ...

MyClass* myClass = new MyClass();
myClass->MyField = "Hello world!";

2) Sans le nouvea mot-clé ...

MyClass myClass;
myClass.MyField = "Hello world!";

Du point de vue de la mise en œuvre, elles ne semblent pas si différentes (mais je suis sûre qu'elles le sont) ... Cependant, mon langage principal est le C # et, bien sûr, la première méthode est celle à laquelle je suis habitué.

La difficulté semble être que la méthode 1 est plus difficile à utiliser avec les classes std C++.

Quelle méthode devrais-je utiliser?

Mise à jour 1:

J'ai récemment utilisé le nouvea mot-clé pour tas mémoire (ou free store) pour un grand tableau qui sortait de la portée (c'est-à-dire renvoyé de une fonction). Auparavant, lorsque j'utilisais la pile, ce qui provoquait la corruption de la moitié des éléments en dehors de la portée, le passage à l'utilisation de tas a permis de s'assurer que les éléments étaient intacts. Yay!

Mise à jour 2:

Un de mes amis m'a récemment dit qu'il existe une règle simple pour utiliser le mot clé new. chaque fois que vous tapez new, tapez delete.

Foobar *foobar = new Foobar();
delete foobar; // TODO: Move this to the right place.

Cela aide à prévenir les fuites de mémoire, car vous devez toujours placer la suppression quelque part (c'est-à-dire lorsque vous la coupez et la collez dans un destructeur ou autrement).

254
Nick Bolton

Méthode 1 (utilisant new)

  • Alloue de la mémoire pour l’objet sur le magasin gratuit (C’est souvent la même chose que le tas )
  • Vous oblige à explicitement delete votre objet ultérieurement. (Si vous ne le supprimez pas, vous pourriez créer une fuite de mémoire.)
  • La mémoire reste allouée jusqu'à ce que vous delete le. (c'est-à-dire que vous pourriez return un objet que vous avez créé à l'aide de new)
  • L'exemple de la question va fuite de mémoire sauf si le pointeur est deleted; et il devrait toujours être supprimé , quel que soit le chemin de contrôle utilisé ou si des exceptions sont levées.

Méthode 2 (n'utilise pas new)

  • Alloue de la mémoire pour l'objet sur la pile (où toutes les variables locales vont) Il y a généralement moins de mémoire disponible pour la pile; Si vous allouez trop d'objets, vous risquez un débordement de pile.
  • Vous n'aurez pas besoin de delete plus tard.
  • La mémoire n'est plus allouée lorsqu'elle sort de la portée. (c’est-à-dire que vous ne devriez pas return un pointeur sur un objet de la pile)

En ce qui concerne lequel utiliser; vous choisissez la méthode qui vous convient le mieux, compte tenu des contraintes ci-dessus.

Quelques cas simples:

  • Si vous ne souhaitez pas appeler delete (et le potentiel de provoquer fuites de mémoire ), vous ne devez pas utiliser new.
  • Si vous souhaitez renvoyer un pointeur sur votre objet à partir d'une fonction, vous devez utiliser new.
286
Daniel LeCheminant

Il y a une différence importante entre les deux.

Tout ce qui n'est pas alloué avec new se comporte beaucoup comme les types de valeur en C # (et on dit souvent que ces objets sont alloués sur la pile, ce qui est probablement le cas le plus courant/évident, mais pas toujours vrai. Plus précisément, les objets alloués sans utiliser new have durée de stockage automatique Tout ce qui est alloué avec new est alloué sur le segment de mémoire et un pointeur est renvoyé, exactement comme les types de référence en C #.

Tout ce qui est alloué sur la pile doit avoir une taille constante, déterminée lors de la compilation (le compilateur doit définir correctement le pointeur de la pile ou, si l'objet est membre d'une autre classe, il doit en ajuster la taille). . C'est pourquoi les tableaux en C # sont des types de référence. Ils doivent l'être, car avec les types de référence, nous pouvons décider au moment de l'exécution combien de mémoire demander. Et la même chose s'applique ici. Seuls les tableaux de taille constante (taille pouvant être déterminée au moment de la compilation) peuvent être alloués avec une durée de stockage automatique (sur la pile). Des tableaux de taille dynamique doivent être alloués sur le tas, en appelant new.

(Et c'est là que s'arrête toute similitude avec C #)

Maintenant, tout ce qui est alloué sur la pile a une durée de stockage "automatique" (vous pouvez en fait déclarer une variable sous la forme auto, mais c'est la valeur par défaut si aucun autre type de stockage n'est spécifié et le mot-clé n'est pas vraiment utilisé dans la pratique, mais c'est de là que ça vient)

La durée de stockage automatique signifie exactement ce que cela ressemble, la durée de la variable est gérée automatiquement. En revanche, tout ce qui est alloué sur le tas doit être supprimé manuellement par vous. Voici un exemple:

void foo() {
  bar b;
  bar* b2 = new bar();
}

Cette fonction crée trois valeurs à considérer:

Sur la ligne 1, il déclare une variable b de type bar sur la pile (durée automatique).

Sur la ligne 2, il déclare un pointeur barb2 sur la pile (durée automatique), et appelle new, en affectant un objet bar au tas. (durée dynamique)

Au retour de la fonction, il se passera ce qui suit: D'abord, b2 sort du cadre (l'ordre de destruction est toujours opposé à l'ordre de construction). Mais b2 est juste un pointeur, donc rien ne se passe, la mémoire qu’il occupe est simplement libérée. Et surtout, la mémoire elle pointe vers (l'instance bar sur le tas) n'est PAS touchée. Seul le pointeur est libéré, car seul le pointeur avait une durée automatique. Deuxièmement, b sort de la portée. Comme il a une durée automatique, son destructeur est appelé et la mémoire est libérée.

Et le barinstance sur le tas? C'est probablement toujours là. Personne n'a pris la peine de le supprimer, nous avons donc perdu de la mémoire.

Dans cet exemple, nous pouvons voir que tout ce qui a une durée automatique est garanti avoir son destructeur appelé quand il sort de la portée. C'est utile. Mais tout ce qui est alloué sur le tas dure aussi longtemps que nous en avons besoin et peut être dimensionné de manière dynamique, comme dans le cas des tableaux. C'est aussi utile. Nous pouvons l'utiliser pour gérer nos allocations de mémoire. Et si la classe Foo allouait de la mémoire sur le tas dans son constructeur et la supprimait dans son destructeur. Ensuite, nous pourrions obtenir le meilleur des deux mondes: des allocations de mémoire sécurisées dont la libération sera garantie, mais sans la limitation de tout forcer.

Et c'est à peu près exactement comment fonctionne la plupart du code C++. Regardez le std::vector de la bibliothèque standard, par exemple. Cela est généralement alloué sur la pile, mais peut être dimensionné et redimensionné de manière dynamique. Et cela se fait en allouant en interne de la mémoire sur le tas si nécessaire. L'utilisateur de la classe ne voit jamais cela, il n'y a donc aucune chance de perdre de la mémoire ou d'oublier de nettoyer ce que vous avez alloué.

Ce principe s’appelle RAII (l’acquisition des ressources est une initialisation) et peut être étendu à toutes les ressources devant être acquises et libérées. (sockets réseau, fichiers, connexions à la base de données, verrous de synchronisation). Tous peuvent être acquis dans le constructeur et libérés dans le destructeur, vous avez ainsi la garantie que toutes les ressources acquises seront libérées.

En règle générale, n'utilisez jamais new/delete directement à partir de votre code de niveau supérieur. Emballez-le toujours dans une classe capable de gérer la mémoire pour vous et de s'assurer qu'elle sera libérée à nouveau. (Oui, il peut y avoir des exceptions à cette règle. En particulier, les pointeurs intelligents vous obligent à appeler new directement et à passer le pointeur à son constructeur, qui prend ensuite le relais et garantit que delete est appelé correctement. Mais c’est toujours une règle très importante)

112
jalf

Quelle méthode devrais-je utiliser?

Cela n’est presque jamais déterminé par vos préférences de frappe, mais par le contexte. Si vous devez conserver l'objet sur plusieurs piles ou s'il est trop lourd pour la pile, vous l'allouez sur le magasin gratuit. De plus, puisque vous allouez un objet, vous êtes également responsable de la libération de la mémoire. Recherchez l'opérateur delete.

Pour alléger le fardeau lié à l'utilisation de la gestion de magasin gratuit, les gens ont inventé des choses comme auto_ptr et unique_ptr. Je vous recommande fortement de jeter un coup d'oeil à ceux-ci. Ils pourraient même vous aider à résoudre vos problèmes de dactylographie ;-)

13
dirkgently

Si vous écrivez en C++, vous écrivez probablement pour des performances. L'utilisation de new et de la librairie gratuite est beaucoup plus lente que celle de la pile (en particulier lorsque vous utilisez des threads), utilisez-la uniquement lorsque vous en avez besoin.

Comme d'autres l'ont dit, vous avez besoin de new lorsque votre objet doit vivre en dehors de la fonction ou de la portée de l'objet, qu'il est vraiment volumineux ou que vous ne connaissez pas la taille d'un tableau au moment de la compilation.

Aussi, essayez d'éviter de jamais utiliser delete. Enveloppez votre nouveau dans un pointeur intelligent à la place. Laissez l’appel du pointeur intelligent supprimer pour vous.

Dans certains cas, un pointeur intelligent n'est pas intelligent. Ne stockez jamais std :: auto_ptr <> dans un conteneur STL. Il supprimera le pointeur trop tôt à cause d'opérations de copie à l'intérieur du conteneur. Un autre cas est lorsque vous avez un très grand conteneur STL de pointeurs sur des objets. boost :: shared_ptr <> aura une tonne de temps système car il augmente les décomptes de référence. La meilleure façon de procéder dans ce cas est de placer le conteneur STL dans un autre objet et de lui donner un destructeur qui appellera delete sur chaque pointeur du conteneur.

9
Zan Lynx

La réponse courte est: si vous êtes débutant en C++, vous devriez ne jamais utiliser new ou delete vous-même.

Vous devez plutôt utiliser des pointeurs intelligents tels que std::unique_ptr et std::make_unique (ou moins souvent, std::shared_ptr et std::make_shared). De cette façon, vous n'avez pas à vous soucier autant des fuites de mémoire. Et même si vous êtes plus avancé, la meilleure pratique consiste généralement à encapsuler la manière dont vous utilisez new et delete dans une petite classe (telle qu'un pointeur intelligent personnalisé) qui est uniquement dédiée. objecter les problèmes de cycle de vie.

Bien entendu, en arrière-plan, ces pointeurs intelligents effectuent toujours l'allocation dynamique et la désallocation, de sorte que le code les utilisant aurait toujours le surcoût d'exécution associé. D'autres réponses ici couvrent ces questions, et comment décider du moment d'utilisation des pointeurs intelligents plutôt que de simplement créer des objets sur la pile ou de les incorporer en tant que membres directs d'un objet, suffisamment pour que je ne les répète pas. Mais mon résumé serait le suivant: n'utilisez pas de pointeurs intelligents ni d'allocation dynamique tant que rien ne vous y obligera.

6
Daniel Schepler

Sans le mot clé new, vous le stockez sur pile d'appels . Le stockage de variables trop volumineuses sur la pile entraînera débordement de la pile .

2
vartec

Passez-vous myClass d'une fonction ou attendez-vous qu'elle existe en dehors de cette fonction? Comme d'autres l'ont dit, tout est question de portée lorsque vous n'allouez pas sur le tas. Lorsque vous quittez la fonction, elle disparaît (éventuellement). Une des erreurs classiques commises par les débutants est la tentative de créer un objet local d’une classe dans une fonction et de le renvoyer sans l’allouer sur le tas. Je me souviens du débogage de ce genre de choses à mes débuts en faisant c ++.

1
itsmatt

Si votre variable n’est utilisée que dans le contexte d’une seule fonction, il est préférable d’utiliser une variable de pile, c’est-à-dire l’option 2. Comme d’autres l'ont déjà dit, vous n'avez pas à gérer la durée de vie des variables de pile: elles sont construites et détruit automatiquement. De plus, l'allocation/désallocation d'une variable sur le tas est lente en comparaison. Si votre fonction est appelée assez souvent, vous constaterez une amélioration considérable des performances si vous utilisez des variables de pile par rapport à des variables de segment de mémoire.

Cela dit, il existe quelques cas évidents où les variables de pile sont insuffisantes.

Si la variable de pile a une empreinte mémoire importante, vous courez le risque de déborder de la pile. Par défaut, la taille de pile de chaque thread est de 1 Mo sous Windows. Il est peu probable que vous créiez une variable de pile d'une taille de 1 Mo, mais vous devez garder à l'esprit que l'utilisation de la pile est cumulative. Si votre fonction appelle une fonction qui appelle une autre fonction qui appelle une autre fonction qui ..., les variables de pile de toutes ces fonctions occupent de l'espace sur la même pile. Les fonctions récursives peuvent rapidement rencontrer ce problème, en fonction de la profondeur de la récursivité. Si cela pose un problème, vous pouvez augmenter la taille de la pile (non recommandé) ou allouer la variable sur le tas à l'aide de l'opérateur new (recommandé).

L'autre condition, plus probable, est que votre variable doit "vivre" au-delà de la portée de votre fonction. Dans ce cas, vous alloueriez la variable sur le segment de mémoire de sorte qu'elle puisse être atteinte en dehors de la portée d'une fonction donnée.

1
Matt Davis

La réponse simple est oui - new () crée un objet sur le tas (avec l’effet secondaire malheureux que vous devez gérer sa durée de vie (en appelant explicitement delete sur celui-ci), alors que le second formulaire crée un objet dans la pile de la pile courante. portée et cet objet sera détruit quand il va hors de portée.

1
Timo Geusch

La réponse courte est oui, le "nouveau" mot-clé est extrêmement important, car lorsque vous l'utilisez, les données de l'objet sont stockées sur le tas plutôt que sur la pile, ce qui est le plus important!

0
RAGNO

La deuxième méthode crée l'instance sur la pile, avec quelque chose comme quelque chose déclarée int et la liste des paramètres qui sont passés à la fonction.

La première méthode fait de la place pour un pointeur sur la pile, que vous avez définie à l'emplacement en mémoire où un nouveau MyClass a été alloué sur le tas ou le magasin libre.

La première méthode nécessite également que vous delete ce que vous créiez avec new, tandis que dans la seconde méthode, la classe est automatiquement détruite et libérée lorsqu'elle sort de la portée (généralement l'accolade de fermeture suivante).

0
greyfade