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?
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!
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).
Méthode 1 (utilisant new
)
delete
votre objet ultérieurement. (Si vous ne le supprimez pas, vous pourriez créer une fuite de mémoire.)delete
le. (c'est-à-dire que vous pourriez return
un objet que vous avez créé à l'aide de new
)delete
d; 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
)
delete
plus tard.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:
delete
(et le potentiel de provoquer fuites de mémoire ), vous ne devez pas utiliser new
.new
.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 bar
b2
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 bar
instance 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)
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 ;-)
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.
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.
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 .
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 ++.
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.
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.
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!
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).