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


273

J'utilise C ++ depuis peu de temps et je me suis interrogé sur le nouveau mot clé. Dois-je simplement l'utiliser ou non?

1) Avec le nouveau mot-clé ...

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

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

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

Du point de vue de l'implémentation, ils ne semblent pas si différents (mais je suis sûr qu'ils le sont) ... Cependant, mon langage principal est C #, et bien sûr la 1ère méthode est ce à quoi je suis habitué.

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

Quelle méthode dois-je utiliser?

Mise à jour 1:

J'ai récemment utilisé le nouveau mot clé pour la mémoire de tas (ou magasin gratuit ) pour un grand tableau qui sortait du cadre (c'est-à-dire qui était renvoyé par une fonction). Là où auparavant 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 du tas garantissait que les éléments étaient en contact. Yay!

Mise à jour 2:

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

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

Cela permet d'éviter les fuites de mémoire, car vous devez toujours placer la suppression quelque part (c'est-à-dire lorsque vous la coupez et la collez sur un destructeur ou autre).


6
La réponse courte est, utilisez la version courte lorsque vous pouvez vous en tirer. :)
jalf

11
Une meilleure technique que d'écrire toujours une suppression correspondante - utilisez des conteneurs STL et des pointeurs intelligents comme std::vectoret std::shared_ptr. Ceux-ci enveloppent les appels vers newet deletepour vous, de sorte que vous êtes encore moins susceptible de perdre de la mémoire. Demandez-vous par exemple: vous souvenez-vous toujours de mettre un correspondant deletepartout où une exception pourrait être levée? Mettre des deletes à la main est plus difficile que vous ne le pensez.
AshleysBrain

@nbolton Re: UPDATE 1 - L'une des belles choses à propos de C ++ est qu'il vous permet de stocker des types définis par l'utilisateur sur la pile, tandis que les langages récupérés comme C # vous obligent à stocker les données sur le tas . Le stockage de données sur le tas consomme plus de ressources que le stockage de données sur la pile , vous devez donc préférer la pile au tas , sauf lorsque votre UDT nécessite une grande quantité de mémoire pour stocker ses données. (Cela signifie également que les objets sont passés par valeur par défaut). Une meilleure solution à votre problème serait de passer le tableau à la fonction par référence .
Charles Addis du

Réponses:


304

Méthode 1 (en utilisant new)

  • Alloue de la mémoire pour l'objet sur la boutique gratuite (c'est souvent la même chose que le tas )
  • Vous oblige à explicitement deletevotre objet plus tard. (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. (c.-à-d. vous pourriez returnun objet que vous avez créé en utilisant new)
  • L'exemple de la question entraînera une fuite de mémoire sauf si le pointeur est deleted; et il doit toujours être supprimé , quel que soit le chemin de contrôle utilisé ou si des exceptions sont levées.

Méthode 2 (ne pas utiliser new)

  • Alloue de la mémoire pour l'objet sur la pile (où vont toutes les variables locales) 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'en aurez pas besoin deleteplus tard.
  • La mémoire n'est plus allouée lorsqu'elle est hors de portée. (c'est-à-dire que vous ne devez pas returnpointer vers 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 faciles:

  • Si vous ne voulez pas vous soucier des appels delete(et du risque de provoquer des fuites de mémoire ), vous ne devriez pas utiliser new.
  • Si vous souhaitez renvoyer un pointeur sur votre objet à partir d'une fonction, vous devez utiliser new

4
Un nitpick - je crois que le nouvel opérateur alloue de la mémoire à partir du "magasin libre", tandis que malloc alloue à partir du "tas". Il n'est pas garanti que ce soit la même chose, bien qu'en pratique ce soit le cas. Voir gotw.ca/gotw/009.htm .
Fred Larson

4
Je pense que votre réponse pourrait être plus claire sur laquelle utiliser. (99% du temps, le choix est simple. Utilisez la méthode 2, sur un objet wrapper qui appelle new / delete dans constructeur / destructeur)
jalf

4
@jalf: La méthode 2 est celle qui n'utilise pas la nouvelle: - / Dans tous les cas, il y a de nombreuses fois que vous coderez sera beaucoup plus simple (par exemple en gérant les cas d'erreur) en utilisant la méthode 2 (celle sans la nouvelle)
Daniel LeCheminant

Un autre nitpick ... Vous devriez rendre plus évident que le premier exemple de Nick fuit la mémoire, tandis que son second ne le fait pas, même face aux exceptions.
Arafangion

4
@Fred, Arafangion: Merci pour votre perspicacité; J'ai intégré vos commentaires dans la réponse.
Daniel LeCheminant

118

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

Tout ce qui n'est pas alloué avec newse comporte comme les types de valeur en C # (et les gens disent 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 newont un stockage automatique duration Tout alloué avec newest alloué sur le tas, et un pointeur est retourné, 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 au moment de la compilation (le compilateur doit définir correctement le pointeur de pile, ou si l'objet est membre d'une autre classe, il doit ajuster la taille de cette autre classe) . 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 de la quantité de mémoire à demander. Et la même chose s'applique ici. Seuls les tableaux de taille constante (une taille qui peut être déterminée au moment de la compilation) peuvent être alloués avec une durée de stockage automatique (sur la pile). Les tableaux de taille dynamique doivent être alloués sur le tas, en appelant new.

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

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

La durée de stockage automatique signifie exactement à quoi 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 bde type barsur la pile (durée automatique).

Sur la ligne 2, il déclare un barpointeur b2sur la pile (durée automatique), et appelle new, allouant un barobjet sur le tas. (durée dynamique)

Lorsque la fonction revient, les événements suivants se produisent: Premièrement, b2sort du domaine d'application (l'ordre de destruction est toujours opposé à l'ordre de construction). Mais ce b2n'est qu'un pointeur, donc rien ne se passe, la mémoire qu'il occupe est simplement libérée. Et surtout, la mémoire vers laquelle il pointe (l' barinstance sur le tas) n'est PAS touchée. Seul le pointeur est libéré, car seul le pointeur a une durée automatique. Deuxièmement, bsort du domaine, donc comme il a une durée automatique, son destructeur est appelé et la mémoire est libérée.

Et l' 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.

De cet exemple, nous pouvons voir que tout ce qui a une durée automatique est garanti d'avoir son destructeur appelé quand il sort du domaine. C'est utile. Mais tout ce qui est alloué sur le tas dure aussi longtemps que nous en avons besoin et peut être dimensionné dynamiquement, comme dans le cas des tableaux. C'est également 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 supprimait cette mémoire dans son destructeur. Ensuite, nous pourrions obtenir le meilleur des deux mondes, des allocations de mémoire sûres qui sont garanties d'être à nouveau libérées, mais sans les limitations de forcer tout à être sur la pile.

Et c'est à peu près exactement comment fonctionne la plupart du code C ++. Regardez std::vectorpar exemple la bibliothèque standard . Cela est généralement alloué sur la pile, mais peut être dimensionné et redimensionné dynamiquement. Et il le 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 aucun risque de fuite de mémoire ou d'oubli de nettoyer ce que vous avez alloué.

Ce principe est appelé RAII (Resource Acquisition is Initialization), et il peut être étendu à toute ressource qui doit être acquise et libérée. (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 donc la garantie que toutes les ressources que vous acquérez seront à nouveau libérées.

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


2
"Tout ce qui n'est pas alloué avec new est placé sur la pile" Pas dans les systèmes sur lesquels j'ai travaillé ... généralement les données globales (statiques) intialisées (et non initiées) sont placées dans leurs propres segments. Par exemple, .data, .bss, etc ... segments de l'éditeur de liens. Pedantic, je sais ...
Dan

Bien sûr, vous avez raison. Je ne pensais pas vraiment aux données statiques. Ma mauvaise, bien sûr. :)
jalf

2
Pourquoi tout élément alloué sur la pile doit-il avoir une taille constante?
user541686

Ce n'est pas toujours le cas , il existe plusieurs façons de le contourner, mais dans le cas général, c'est le cas, car il est sur une pile. S'il se trouve en haut de la pile, il peut être possible de le redimensionner, mais une fois que quelque chose d'autre est poussé dessus, il est "muré", entouré d'objets de chaque côté, il ne peut donc pas vraiment être redimensionné . Oui, dire qu'il doit toujours avoir une taille fixe est un peu une simplification, mais il transmet l'idée de base (et je ne recommanderais pas de jouer avec les fonctions C qui vous permettent d'être trop créatif avec les allocations de pile)
jalf

14

Quelle méthode dois-je utiliser?

Ceci 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 la boutique gratuite. De plus, puisque vous allouez un objet, vous êtes également responsable de libérer la mémoire. Recherchez l' deleteopérateur.

Pour alléger le fardeau de l'utilisation de la gestion des magasins gratuits, les gens ont inventé des trucs comme auto_ptret unique_ptr. Je vous recommande fortement de les consulter. Ils pourraient même être utiles pour vos problèmes de frappe ;-)


10

Si vous écrivez en C ++, vous écrivez probablement pour des performances. L'utilisation de new et de la boutique gratuite est beaucoup plus lente que l'utilisation 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 nouveautés lorsque votre objet doit vivre en dehors de la portée de la fonction ou de l'objet, l'objet est vraiment grand ou lorsque vous ne connaissez pas la taille d'un tableau au moment de la compilation.

Essayez également d'éviter de supprimer. Enveloppez plutôt votre nouveau dans un pointeur intelligent. 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 en raison d'opérations de copie à l'intérieur du conteneur. Un autre cas est celui où vous avez un très grand conteneur STL de pointeurs vers des objets. boost :: shared_ptr <> aura une surcharge de vitesse car il augmente le nombre de références de haut en bas. La meilleure façon de procéder dans ce cas est de placer le conteneur STL dans un autre objet et de donner à cet objet un destructeur qui appellera delete sur chaque pointeur du conteneur.


10

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

Au lieu de cela, vous devez utiliser des pointeurs intelligents tels que std::unique_ptret std::make_unique(ou moins souvent, std::shared_ptret 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 personnalisée que vous utilisez newet deletedans une petite classe (comme un pointeur intelligent personnalisé) dédiée uniquement aux problèmes de cycle de vie des objets.

Bien sûr, en arrière-plan, ces pointeurs intelligents effectuent toujours l'allocation dynamique et la désallocation, de sorte que le code qui les utilise aurait toujours le temps d'exécution associé. D'autres réponses ici ont couvert ces problèmes et comment prendre des décisions de conception sur le moment d'utiliser des pointeurs intelligents par rapport à la simple création d'objets sur la pile ou à leur incorporation en tant que membres directs d'un objet, suffisamment pour que je ne les répète pas. Mais mon résumé serait: n'utilisez pas de pointeurs intelligents ou d'allocation dynamique jusqu'à ce que quelque chose vous y oblige.


intéressant de voir comment une réponse peut changer au fil du temps;)
Wolf


2

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 dessus), tandis que le second formulaire crée un objet dans la pile dans le courant) portée et cet objet sera détruit quand il sort de la portée.


1

Si votre variable n'est utilisée que dans le contexte d'une seule fonction, il vaut mieux utiliser une variable de pile, c'est-à-dire l'option 2. Comme d'autres l'ont dit, vous n'avez pas à gérer la durée de vie des variables de pile - elles sont construites et détruit automatiquement. En outre, 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 aux variables de tas.

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

Si la variable de pile a une grande empreinte mémoire, vous courez le risque de déborder 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 dans toutes ces fonctions prennent de la place sur la même pile. Les fonctions récursives peuvent rencontrer rapidement ce problème, selon la profondeur de la récursivité. En cas de problème, vous pouvez augmenter la taille de la pile (non recommandé) ou allouer la variable sur le tas à l'aide du nouvel opérateur (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 tas afin qu'elle puisse être atteinte en dehors de la portée d'une fonction donnée.


1

Vous passez myClass hors d'une fonction, ou vous attendez à ce qu'elle existe en dehors de cette fonction? Comme certains l'ont dit, il s'agit de portée lorsque vous n'allouez pas sur le tas. Lorsque vous quittez la fonction, elle disparaît (éventuellement). L'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 avoir débogué ce genre de chose dans mes premiers jours en faisant C ++.


0

La deuxième méthode crée l'instance sur la pile, ainsi que des éléments déclarés intet la liste des paramètres passés dans la fonction.

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

La première méthode requiert également que vous deletecréez avec new, tandis que dans la deuxième méthode, la classe est automatiquement détruite et libérée lorsqu'elle tombe hors de portée (l'accolade de fermeture suivante, généralement).


-1

La réponse courte est oui, le "nouveau" mot clé est incroyablement important car lorsque vous l'utilisez, les données d'objet sont stockées sur le tas par opposition à la pile, ce qui est le plus important!

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.