Pourquoi les programmeurs C ++ devraient-ils minimiser l'utilisation de «nouveau»?


873

Je suis tombé sur une question de débordement de pile Fuite de mémoire avec std :: string lors de l'utilisation de std :: list <std :: string> , et l' un des commentaires dit ceci:

Arrêtez d'utiliser newautant. Je ne vois aucune raison pour laquelle vous avez utilisé du neuf partout où vous l'avez fait. Vous pouvez créer des objets par valeur en C ++ et c'est l'un des énormes avantages de l'utilisation du langage.
Vous n'avez pas à tout allouer sur le tas.
Arrêtez de penser comme un programmeur Java .

Je ne sais pas vraiment ce qu'il veut dire par là.

Pourquoi les objets devraient-ils être créés par valeur en C ++ aussi souvent que possible, et quelle différence cela fait-il en interne?
Ai-je mal interprété la réponse?

Réponses:


1037

Il existe deux techniques d'allocation de mémoire largement utilisées: l'allocation automatique et l'allocation dynamique. Généralement, il existe une région de mémoire correspondante pour chacun: la pile et le tas.

Empiler

La pile alloue toujours la mémoire de manière séquentielle. Il peut le faire car il vous oblige à libérer la mémoire dans l'ordre inverse (First-In, Last-Out: FILO). Il s'agit de la technique d'allocation de mémoire pour les variables locales dans de nombreux langages de programmation. Il est très, très rapide car il nécessite une comptabilité minimale et la prochaine adresse à allouer est implicite.

En C ++, cela s'appelle le stockage automatique car le stockage est revendiqué automatiquement à la fin de la portée. Dès que l'exécution du bloc de code actuel (délimité par {}) est terminée, la mémoire de toutes les variables de ce bloc est automatiquement collectée. C'est également le moment où les destructeurs sont appelés pour nettoyer les ressources.

Tas

Le tas permet un mode d'allocation de mémoire plus flexible. La comptabilité est plus complexe et l'allocation est plus lente. Puisqu'il n'y a pas de point de libération implicite, vous devez libérer la mémoire manuellement, en utilisant deleteou delete[]( freeen C). Cependant, l'absence d'un point de sortie implicite est la clé de la flexibilité du tas.

Raisons d'utiliser l'allocation dynamique

Même si l'utilisation du segment de mémoire est plus lente et peut entraîner des fuites de mémoire ou une fragmentation de la mémoire, il existe des cas d'utilisation parfaits pour l'allocation dynamique, car elle est moins limitée.

Deux raisons principales d'utiliser l'allocation dynamique:

  • Vous ne savez pas de combien de mémoire vous avez besoin au moment de la compilation. Par exemple, lorsque vous lisez un fichier texte dans une chaîne, vous ne savez généralement pas quelle est la taille du fichier, vous ne pouvez donc pas décider de la quantité de mémoire à allouer avant d'exécuter le programme.

  • Vous souhaitez allouer de la mémoire qui persistera après avoir quitté le bloc actuel. Par exemple, vous souhaiterez peut-être écrire une fonction string readfile(string path)qui renvoie le contenu d'un fichier. Dans ce cas, même si la pile pouvait contenir tout le contenu du fichier, vous ne pouviez pas revenir d'une fonction et conserver le bloc de mémoire alloué.

Pourquoi l'allocation dynamique est souvent inutile

En C ++, il y a une construction soignée appelée destructeur . Ce mécanisme vous permet de gérer les ressources en alignant la durée de vie de la ressource avec la durée de vie d'une variable. Cette technique est appelée RAII et est le point distinctif du C ++. Il "enveloppe" les ressources en objets. std::stringest un parfait exemple. Cet extrait:

int main ( int argc, char* argv[] )
{
    std::string program(argv[0]);
}

alloue en fait une quantité variable de mémoire. L' std::stringobjet alloue de la mémoire à l'aide du tas et la libère dans son destructeur. Dans ce cas, vous n'avez pas eu besoin de gérer manuellement les ressources et bénéficiez quand même des avantages de l'allocation dynamique de mémoire.

En particulier, cela implique que dans cet extrait:

int main ( int argc, char* argv[] )
{
    std::string * program = new std::string(argv[0]);  // Bad!
    delete program;
}

il y a une allocation de mémoire dynamique inutile. Le programme nécessite plus de frappe (!) Et présente le risque d'oublier de désallouer la mémoire. Il le fait sans aucun avantage apparent.

Pourquoi utiliser le stockage automatique aussi souvent que possible

Fondamentalement, le dernier paragraphe le résume. Utiliser le stockage automatique aussi souvent que possible rend vos programmes:

  • plus rapide à taper;
  • plus rapide lors de l'exécution;
  • moins sujettes aux fuites de mémoire / ressources.

Points bonus

Dans la question référencée, il existe des préoccupations supplémentaires. En particulier, la classe suivante:

class Line {
public:
    Line();
    ~Line();
    std::string* mString;
};

Line::Line() {
    mString = new std::string("foo_bar");
}

Line::~Line() {
    delete mString;
}

Est en fait beaucoup plus risqué à utiliser que le suivant:

class Line {
public:
    Line();
    std::string mString;
};

Line::Line() {
    mString = "foo_bar";
    // note: there is a cleaner way to write this.
}

La raison en est que std::stringdéfinit correctement un constructeur de copie. Considérez le programme suivant:

int main ()
{
    Line l1;
    Line l2 = l1;
}

En utilisant la version d'origine, ce programme se bloquera probablement, car il utilise deletedeux fois la même chaîne. En utilisant la version modifiée, chaque Lineinstance possédera sa propre instance de chaîne , chacune avec sa propre mémoire et les deux seront publiées à la fin du programme.

Autres notes

L'utilisation intensive de RAII est considérée comme une meilleure pratique en C ++ pour toutes les raisons ci-dessus. Cependant, il existe un avantage supplémentaire qui n'est pas immédiatement évident. Fondamentalement, c'est mieux que la somme de ses parties. L'ensemble du mécanisme compose . Il évolue.

Si vous utilisez la Lineclasse comme bloc de construction:

 class Table
 {
      Line borders[4];
 };

alors

 int main ()
 {
     Table table;
 }

alloue quatre std::stringinstances, quatre Lineinstances, une Tableinstance et tout le contenu de la chaîne et tout est libéré automatiquement .


55
+1 pour avoir mentionné RAII à la fin, mais il devrait y avoir quelque chose à propos des exceptions et du déroulement de la pile.
Tobu

7
@Tobu: oui, mais ce post est déjà assez long, et je voulais le garder plutôt concentré sur la question d'OP. Je finirai par écrire un article de blog ou quelque chose et un lien vers celui-ci à partir d'ici.
André Caron

15
Ce serait un excellent complément pour mentionner les inconvénients de l'allocation de pile (au moins jusqu'à C ++ 1x) - vous devez souvent copier les choses inutilement si vous ne faites pas attention. par exemple, un Monstercrache un Treasureau Worldmoment de sa mort. Dans sa Die()méthode, il ajoute le trésor au monde. Il doit l'utiliser world->Add(new Treasure(/*...*/))dans d'autres pour conserver le trésor après sa mort. Les alternatives sont shared_ptr(peuvent être exagérées), auto_ptr(mauvaise sémantique pour le transfert de propriété), passer par la valeur (gaspillage) et move+ unique_ptr(pas encore largement mises en œuvre).
kizzx2

7
Ce que vous avez dit sur les variables locales allouées par pile peut être un peu trompeur. "La pile" fait référence à la pile d'appels, qui stocke les trames de pile . Ce sont ces cadres de pile qui sont stockés à la mode LIFO. Les variables locales pour une trame spécifique sont allouées comme si elles étaient membres d'une structure.
someguy

7
@someguy: En effet, l'explication n'est pas parfaite. L'implémentation a la liberté dans sa politique d'allocation. Cependant, les variables doivent être initialisées et détruites de façon LIFO, donc l'analogie est vraie. Je ne pense pas que cela complique davantage la réponse.
André Caron

171

Parce que la pile est plus rapide et étanche

En C ++, il suffit d'une seule instruction pour allouer de l'espace - sur la pile - pour chaque objet de portée locale dans une fonction donnée, et il est impossible de divulguer une partie de cette mémoire. Ce commentaire avait l'intention (ou aurait dû avoir l'intention) de dire quelque chose comme "utilisez la pile et non le tas".


18
"il ne faut qu'une seule instruction pour allouer de l'espace" - oh, un non-sens. Bien sûr, il ne faut qu'une seule instruction à ajouter au pointeur de pile, mais si la classe a une structure interne intéressante, il y aura beaucoup plus que l'ajout au pointeur de pile en cours. Il est également valable de dire qu'en Java, il n'y a pas d'instructions pour allouer de l'espace, car le compilateur gérera les références au moment de la compilation.
Charlie Martin

31
@Charlie a raison. Les variables automatiques sont rapides et infaillibles seraient plus précises.
Oliver Charlesworth

28
@Charlie: Les internes de classe doivent être configurés de toute façon. La comparaison est en cours sur l'allocation de l'espace requis.
Oliver Charlesworth

51
toux int x; return &x;
peterchen

16
vite oui. Mais certainement pas infaillible. Rien n'est infaillible. Vous pouvez obtenir un StackOverflow :)
rxantos

107

La raison en est compliquée.

Tout d'abord, C ++ n'est pas récupéré. Par conséquent, pour chaque nouveau, il doit y avoir une suppression correspondante. Si vous ne parvenez pas à insérer cette suppression, vous avez une fuite de mémoire. Maintenant, pour un cas simple comme celui-ci:

std::string *someString = new std::string(...);
//Do stuff
delete someString;

C’est simple. Mais que se passe-t-il si "Do stuff" lève une exception? Oups: fuite de mémoire. Que se passe-t-il si "Do stuff" émet returntôt? Oups: fuite de mémoire.

Et c'est pour le cas le plus simple . S'il vous arrive de renvoyer cette chaîne à quelqu'un, il doit maintenant la supprimer. Et s'ils la transmettent comme argument, la personne qui la reçoit doit-elle la supprimer? Quand devraient-ils le supprimer?

Ou, vous pouvez simplement faire ceci:

std::string someString(...);
//Do stuff

Non delete. L'objet a été créé sur la "pile", et il sera détruit une fois hors de portée. Vous pouvez même renvoyer l'objet, transférant ainsi son contenu à la fonction appelante. Vous pouvez passer l'objet à des fonctions (généralement comme référence ou const-référence:. void SomeFunc(std::string &iCanModifyThis, const std::string &iCantModifyThis)Et ainsi de suite.

Le tout sans newet delete. Il n'est pas question de savoir qui possède la mémoire ou qui est responsable de sa suppression. Si tu fais:

std::string someString(...);
std::string otherString;
otherString = someString;

Il est entendu que otherStringpossède une copie des données de someString. Ce n'est pas un pointeur; c'est un objet séparé. Il se peut qu'ils aient le même contenu, mais vous pouvez en modifier un sans affecter l'autre:

someString += "More text.";
if(otherString == someString) { /*Will never get here */ }

Vous voyez l'idée?


1
Sur cette note ... Si un objet est alloué dynamiquement dans main(), existe pour la durée du programme, ne peut pas être facilement créé sur la pile en raison de la situation, et des pointeurs vers celui-ci sont passés à toutes les fonctions qui nécessitent un accès à celui-ci , cela peut-il provoquer une fuite en cas de plantage du programme, ou serait-ce sûr? Je supposerais ce dernier, car le système d'exploitation désallouant toute la mémoire du programme devrait également le désallouer logiquement, mais je ne veux rien supposer en ce qui concerne new.
Justin Time - Rétablir Monica le

4
@JustinTime Vous n'avez pas à vous soucier de libérer de la mémoire d'objets alloués dynamiquement qui doivent rester pendant la durée de vie du programme. Lorsqu'un programme s'exécute, le système d'exploitation crée un atlas de mémoire physique, ou mémoire virtuelle, pour lui. Chaque adresse dans l'espace de mémoire virtuelle est mappée à une adresse de mémoire physique, et lorsque le programme se ferme, tout ce qui est mappé à sa mémoire virtuelle est libéré. Ainsi, tant que le programme se ferme complètement, vous n'avez pas à vous soucier de la mémoire allouée qui ne sera jamais supprimée.
Aiman ​​Al-Eryani

75

Les objets créés par newdoivent finir par deletene pas fuir. Le destructeur ne sera pas appelé, la mémoire ne sera pas libérée, le tout. Comme C ++ n'a pas de récupération de place, c'est un problème.

Les objets créés par la valeur (c'est-à-dire sur la pile) meurent automatiquement lorsqu'ils sortent de la portée. L'appel du destructeur est inséré par le compilateur et la mémoire est automatiquement libérée au retour de la fonction.

Les pointeurs intelligents comme unique_ptr, shared_ptrrésolvent le problème de référence pendant, mais ils nécessitent une discipline de codage et ont d'autres problèmes potentiels (copie, boucles de référence, etc.).

En outre, dans les scénarios fortement multithreads, il newexiste un point de discorde entre les threads; il peut y avoir un impact sur les performances en cas de surutilisation new. La création d'objets de pile est par définition thread locale, car chaque thread a sa propre pile.

L'inconvénient des objets de valeur est qu'ils meurent une fois la fonction hôte revenue - vous ne pouvez pas transmettre une référence à ceux qui reviennent à l'appelant, uniquement en copiant, en retournant ou en se déplaçant par valeur.


9
+1. "Les objets créés par newdoivent être finalement deletepeur de ne pas fuir." - pire encore, new[]doit correspondre delete[], et vous obtenez un comportement indéfini si vous avez de la delete new[]mémoire ou de la delete[] newmémoire - très peu de compilateurs avertissent à ce sujet (certains outils comme Cppcheck le font quand ils le peuvent).
Tony Delroy

3
@TonyDelroy Il y a des situations où le compilateur ne peut pas avertir cela. Si une fonction retourne un pointeur, elle pourrait être créée si new (un seul élément) ou new [].
fbafelipe

32
  • C ++ n'utilise aucun gestionnaire de mémoire par lui-même. D'autres langages comme C #, Java a un garbage collector pour gérer la mémoire
  • Les implémentations C ++ utilisent généralement des routines de système d'exploitation pour allouer la mémoire et trop de nouvelles / suppressions pourraient fragmenter la mémoire disponible
  • Avec n'importe quelle application, si la mémoire est fréquemment utilisée, il est conseillé de la pré-allouer et de la libérer lorsqu'elle n'est pas nécessaire.
  • Une mauvaise gestion de la mémoire peut entraîner des fuites de mémoire et il est vraiment difficile à suivre. Ainsi, l'utilisation d'objets de pile dans le cadre de la fonction est une technique éprouvée
  • L'inconvénient de l'utilisation d'objets de pile est qu'il crée plusieurs copies d'objets lors du retour, du passage aux fonctions, etc. Cependant, les compilateurs intelligents sont bien conscients de ces situations et ils ont été bien optimisés pour les performances.
  • C'est vraiment fastidieux en C ++ si la mémoire est allouée et libérée à deux endroits différents. La responsabilité de la libération est toujours une question et nous nous appuyons principalement sur des pointeurs, des objets de pile (maximum possible) et des techniques comme auto_ptr (objets RAII) généralement accessibles.
  • La meilleure chose est que vous avez le contrôle de la mémoire et le pire est que vous n'aurez aucun contrôle sur la mémoire si nous employons une mauvaise gestion de la mémoire pour l'application. Les plantages causés par des corruptions de mémoire sont les plus méchants et les plus difficiles à retracer.

5
En fait, toute langue qui alloue de la mémoire a un gestionnaire de mémoire, y compris c. La plupart sont juste très simples, c'est-à-dire int * x = malloc (4); int * y = malloc (4); ... le premier appel allouera de la mémoire, alias demander à os de la mémoire, (généralement en morceaux 1k / 4k) afin que le deuxième appel n'alloue pas réellement de mémoire, mais vous donne un morceau du dernier morceau pour lequel il a été alloué. IMO, les garbage collector ne sont pas des gestionnaires de mémoire, car il ne gère que la désallocation automatique de la mémoire. Pour être appelé gestionnaire de mémoire, il doit non seulement gérer la désallocation mais également l'allocation de mémoire.
Rahly

1
Les variables locales utilisent la pile afin que le compilateur n'émette pas d'appel à malloc()ou à ses amis pour allouer la mémoire requise. Cependant, la pile ne peut libérer aucun élément de la pile, la seule façon dont la mémoire de la pile est libérée est le déroulement du haut de la pile.
Mikko Rantalainen du

C ++ n'utilise pas de routines de système d'exploitation; cela ne fait pas partie du langage, c'est juste une implémentation courante. C ++ peut même fonctionner sans aucun système d'exploitation.
einpoklum

22

Je vois que quelques raisons importantes pour faire le moins de nouveautés possibles sont manquées:

L'opérateur newa un temps d'exécution non déterministe

L'appel newpeut ou non obliger le système d'exploitation à allouer une nouvelle page physique à votre processus, cela peut être assez lent si vous le faites souvent. Ou il peut déjà avoir un emplacement de mémoire approprié prêt, nous ne savons pas. Si votre programme doit avoir un temps d'exécution cohérent et prévisible (comme dans un système en temps réel ou une simulation de jeu / physique), vous devez éviter newdans vos boucles critiques de temps.

L'opérateur newest une synchronisation de thread implicite

Oui, vous m'avez entendu, votre système d'exploitation doit s'assurer que vos tableaux de pages sont cohérents et, en tant que tel, l'appel newentraînera l'acquisition par votre thread d'un verrou mutex implicite. Si vous appelez régulièrement à newpartir de nombreux threads, vous sérialisez réellement vos threads (je l'ai fait avec 32 processeurs, chacun frappant newpour obtenir quelques centaines d'octets chacun, aïe! C'était un pita royal à déboguer)

Le reste tel que lent, fragmentation, sujet aux erreurs, etc. a déjà été mentionné par d'autres réponses.


2
Les deux peuvent être évités en utilisant le placement new / delete et en allouant la mémoire avant la main. Ou vous pouvez allouer / libérer la mémoire vous-même, puis appeler le constructeur / destructeur. C'est la façon dont std :: vector fonctionne habituellement.
rxantos

1
@rxantos Veuillez lire OP, cette question vise à éviter les allocations de mémoire inutiles. De plus, il n'y a pas de suppression d'emplacements.
Emily L.

@Emily C'est ce que l'OP signifiait, je pense:void * someAddress = ...; delete (T*)someAddress
xryl669

1
L'utilisation de la pile n'est pas non plus déterministe dans le temps d'exécution. Sauf si vous avez appelé mlock()ou quelque chose de similaire. Cela est dû au fait que le système peut manquer de mémoire et qu'il n'y a pas de pages de mémoire physique disponibles pour la pile, de sorte que le système d'exploitation peut avoir besoin d'échanger ou d'écrire des caches (effacer la mémoire sale) sur le disque avant que l'exécution puisse continuer.
Mikko Rantalainen

1
@mikkorantalainen c'est techniquement vrai mais dans une situation de faible mémoire, tous les paris sont de toute façon désactivés par rapport aux performances lorsque vous poussez sur le disque, il n'y a donc rien que vous puissiez faire. Cela n'invalide en aucun cas les conseils pour éviter de nouveaux appels lorsqu'il est raisonnable de le faire.
Emily L.

21

Pré-C ++ 17:

Parce qu'il est sujet à des fuites subtiles même si vous enveloppez le résultat dans un pointeur intelligent .

Considérons un utilisateur "prudent" qui se souvient d'encapsuler des objets dans des pointeurs intelligents:

foo(shared_ptr<T1>(new T1()), shared_ptr<T2>(new T2()));

Ce code est dangereux parce qu'il n'y a aucune garantie que ce soit shared_ptrest construit avant soit T1ou T2. Par conséquent, si l'un new T1()ou new T2()échoue après que l'autre réussit, alors le premier objet sera divulgué car il shared_ptrn'existe pas pour le détruire et le désallouer.

Solution: utiliser make_shared.

Post-C ++ 17:

Ce n'est plus un problème: C ++ 17 impose une contrainte sur l'ordre de ces opérations, en s'assurant dans ce cas que chaque appel à new()doit être immédiatement suivi de la construction du pointeur intelligent correspondant, sans aucune autre opération entre les deux. Cela implique qu'au moment où le second new()est appelé, il est garanti que le premier objet a déjà été encapsulé dans son pointeur intelligent, empêchant ainsi toute fuite en cas de levée d'une exception.

Une autre réponse a fourni une explication plus détaillée du nouvel ordre d'évaluation introduit par C ++ 17 .

Merci à @Remy Lebeau pour avoir signalé que c'est toujours un problème sous C ++ 17 (bien que moins): le shared_ptrconstructeur peut ne pas allouer son bloc de contrôle et lancer, auquel cas le pointeur qui lui est passé n'est pas supprimé.

Solution: utiliser make_shared.


5
Autre solution: n'allouez jamais dynamiquement plus d'un objet par ligne.
Antimony

3
@Antimony: Oui, il est beaucoup plus tentant d'allouer plus d'un objet quand vous en avez déjà alloué un, par rapport à quand vous n'en avez pas alloué.
user541686

1
Je pense qu'une meilleure réponse est que le smart_ptr fuira si une exception est appelée et que rien ne l'attrape.
Natalie Adams

2
Même dans le cas post-C ++ 17, une fuite peut toujours se produire en cas de newsuccès, puis la shared_ptrconstruction suivante échoue. std::make_shared()résoudrait cela aussi
Remy Lebeau

1
@Mehrdad, le shared_ptrconstructeur en question alloue de la mémoire à un bloc de contrôle qui stocke le pointeur et le deleter partagés, donc oui, il peut théoriquement générer une erreur de mémoire. Seuls les constructeurs de copie, de déplacement et d'alias ne sont pas lancés. make_sharedalloue l'objet partagé à l'intérieur du bloc de contrôle lui-même, il n'y a donc qu'une seule allocation au lieu de 2.
Remy Lebeau

17

Dans une large mesure, c'est quelqu'un qui élève ses propres faiblesses à une règle générale. Il n'y a rien de mal en soi à créer des objets à l'aide de l' newopérateur. Il y a un argument pour dire que vous devez le faire avec une certaine discipline: si vous créez un objet, vous devez vous assurer qu'il va être détruit.

La façon la plus simple de le faire est de créer l'objet en stockage automatique, donc C ++ sait le détruire lorsqu'il sort du domaine:

 {
    File foo = File("foo.dat");

    // do things

 }

Maintenant, observez que lorsque vous tombez de ce bloc après l'accolade d'extrémité, il fooest hors de portée. C ++ appellera automatiquement son dtor pour vous. Contrairement à Java, vous n'avez pas besoin d'attendre que le GC le trouve.

Aviez-vous écrit

 {
     File * foo = new File("foo.dat");

vous souhaitez faire correspondre explicitement avec

     delete foo;
  }

ou encore mieux, allouez votre File *comme un "pointeur intelligent". Si vous n'y faites pas attention, cela peut entraîner des fuites.

La réponse elle-même fait l'hypothèse erronée que si vous n'utilisez pas, newvous n'allouez pas sur le tas; en fait, en C ++, vous ne le savez pas. Tout au plus, vous savez qu'une petite quantité de mémoire, disons un pointeur, est certainement allouée sur la pile. Cependant, considérez si l'implémentation de File est quelque chose comme

  class File {
    private:
      FileImpl * fd;
    public:
      File(String fn){ fd = new FileImpl(fn);}

puis FileImplseront encore attribués sur la pile.

Et oui, vous feriez mieux d'être sûr d'avoir

     ~File(){ delete fd ; }

dans la classe aussi; sans cela, vous fuierez la mémoire du tas même si vous ne l'avez apparemment pas alloué du tout.


4
Vous devriez jeter un oeil au code dans la question référencée. Il y a certainement beaucoup de choses qui vont mal dans ce code.
André Caron

7
Je suis d'accord qu'il n'y a rien de mal à utiliser en new soi , mais si vous regardez le code d'origine auquel le commentaire faisait référence, newest abusé. Le code est écrit comme s'il s'agissait de Java ou C #, où il newest utilisé pour pratiquement toutes les variables, lorsque les choses ont beaucoup plus de sens d'être sur la pile.
luke

5
Bon point. Mais les règles générales sont normalement appliquées pour éviter les pièges courants. Qu'il s'agisse d'une faiblesse individuelle ou non, la gestion de la mémoire est suffisamment complexe pour justifier une règle générale comme celle-ci! :)
Robben_Ford_Fan_boy

9
@Charlie: le commentaire ne dit pas que vous ne devriez jamais utiliser new. Il indique que si vous avez le choix entre l'allocation dynamique et le stockage automatique, utilisez le stockage automatique.
André Caron

8
@Charlie: il n'y a rien de mal à utiliser new, mais si vous utilisez delete, vous le faites mal!
Matthieu M.

16

new()ne doit pas être utilisé le moins possible. Il doit être utilisé aussi soigneusement que possible. Et il devrait être utilisé aussi souvent que nécessaire, comme le dictait le pragmatisme.

L'allocation d'objets sur la pile, en s'appuyant sur leur destruction implicite, est un modèle simple. Si la portée requise d'un objet correspond à ce modèle, il n'est pas nécessaire de l'utiliser new(), avec les delete()pointeurs NULL associés et vérifiés. Dans le cas où vous avez beaucoup d'allocation d'objets de courte durée sur la pile, cela devrait réduire les problèmes de fragmentation du tas.

Cependant, si la durée de vie de votre objet doit dépasser la portée actuelle, new()c'est la bonne réponse. Assurez-vous simplement de faire attention à quand et comment vous appelez delete()et aux possibilités des pointeurs NULL, en utilisant des objets supprimés et tous les autres pièges fournis avec l'utilisation de pointeurs.


9
"si la durée de vie de votre objet doit s'étendre au-delà de la portée actuelle, alors new () est la bonne réponse" ... pourquoi ne pas retourner préférentiellement par valeur ou accepter une variable de portée appelante par non- constref ou pointeur ...?
Tony Delroy

2
@Tony: Oui, oui! Je suis content d'entendre quelqu'un préconiser des références. Ils ont été créés pour éviter ce problème.
Nathan Osman

@TonyD ... ou combinez-les: renvoyez un pointeur intelligent par valeur. De cette façon, l'appelant et dans de nombreux cas (c'est-à-dire où make_shared/_uniqueest utilisable) l'appelé n'a jamais besoin de newou delete. Cette réponse manque les vrais points: (A) C ++ fournit des choses comme RVO, déplacer la sémantique et les paramètres de sortie - ce qui signifie souvent que la gestion de la création d'objet et de l'extension de la durée de vie en retournant la mémoire allouée dynamiquement devient inutile et négligente. (B) Même dans les situations où l'allocation dynamique est requise, le stdlib fournit des wrappers RAII qui soulagent l'utilisateur des moindres détails intérieurs.
underscore_d

14

Lorsque vous utilisez new, les objets sont alloués au tas. Il est généralement utilisé lorsque vous prévoyez une expansion. Lorsque vous déclarez un objet tel que,

Class var;

il est placé sur la pile.

Vous devrez toujours appeler destroy sur l'objet que vous avez placé sur le tas avec new. Cela ouvre la possibilité de fuites de mémoire. Les objets placés sur la pile ne sont pas sujets à des fuites de mémoire!


2
+1 "[segment de mémoire] généralement utilisé lorsque vous prévoyez une expansion" - comme l'ajout à un std::stringou std::map, oui, un aperçu perspicace. Ma réaction initiale a été "mais aussi très souvent de découpler la durée de vie d'un objet de la portée du code de création", mais vraiment retourner par valeur ou accepter les valeurs de la portée de l'appelant par non constréférence ou pointeur est mieux pour cela, sauf quand il y a "expansion" impliquée aussi. Il y a cependant d'autres utilisations sonores comme les méthodes d'usine ....
Tony Delroy

12

Une raison notable pour éviter de trop utiliser le tas est pour les performances - impliquant spécifiquement les performances du mécanisme de gestion de la mémoire par défaut utilisé par C ++. Alors que l'allocation peut être assez rapide dans le cas trivial, faire beaucoup newet deletesur des objets de taille non uniforme sans ordre strict entraîne non seulement une fragmentation de la mémoire, mais complique également l'algorithme d'allocation et peut absolument détruire les performances dans certains cas.

C'est le problème que les pools de mémoire ont été créés pour résoudre, ce qui permet d'atténuer les inconvénients inhérents aux implémentations de tas traditionnelles, tout en vous permettant d'utiliser le tas si nécessaire.

Mieux encore, cependant, pour éviter complètement le problème. Si vous pouvez le mettre sur la pile, faites-le.


Vous pouvez toujours allouer une quantité de mémoire raisonnablement énorme, puis utiliser le placement new / delete si la vitesse est un problème.
rxantos

Les pools de mémoire visent à éviter la fragmentation, à accélérer la désallocation (une désallocation pour des milliers d'objets) et à rendre la désallocation plus sûre.
Lothar

10

Je pense que l'affiche voulait dire You do not have to allocate everything on theheapplutôt que le stack.

Fondamentalement, les objets sont alloués sur la pile (si la taille de l'objet le permet, bien sûr) en raison du coût bon marché de l'allocation de pile, plutôt que de l'allocation basée sur le tas qui implique un certain travail par l'allocateur, et ajoute de la verbosité car alors vous devez gérer les données allouées sur le tas.


10

J'ai tendance à être en désaccord avec l'idée d'utiliser de nouveaux «trop». Bien que l'utilisation originale de new avec les classes système par l'affiche originale soit un peu ridicule. ( int *i; i = new int[9999];? vraiment? int i[9999];est beaucoup plus clair.) Je pense que c'est ce qui a attiré la chèvre du commentateur.

Lorsque vous travaillez avec des objets système, il est très rare que vous ayez besoin de plusieurs références au même objet. Tant que la valeur est la même, c'est tout ce qui compte. Et les objets système n'occupent généralement pas beaucoup d'espace en mémoire. (un octet par caractère, dans une chaîne). Et s'ils le font, les bibliothèques doivent être conçues pour prendre en compte cette gestion de la mémoire (si elles sont bien écrites). Dans ces cas, (toutes sauf une ou deux des nouvelles de son code), new est pratiquement inutile et ne sert qu'à introduire des confusions et un potentiel de bugs.

Cependant, lorsque vous travaillez avec vos propres classes / objets (par exemple la classe Line de l'affiche originale), vous devez commencer à penser vous-même aux problèmes tels que l'empreinte mémoire, la persistance des données, etc. À ce stade, autoriser plusieurs références à la même valeur est inestimable - il permet des constructions comme des listes liées, des dictionnaires et des graphiques, où plusieurs variables doivent non seulement avoir la même valeur, mais référencer exactement le même objet en mémoire. Cependant, la classe Line n'a aucune de ces exigences. Ainsi, le code de l'affiche originale n'a absolument aucun besoin new.


habituellement, le nouveau / supprimer serait l'utiliser lorsque vous ne connaissez pas au préalable la taille du tableau. Bien sûr, std :: vector cache nouveau / supprimer pour vous. Vous les utilisez toujours, mais à travers std :: vector. Donc, de nos jours, il serait utilisé lorsque vous ne connaissez pas la taille du tableau et que vous voulez pour une raison quelconque éviter la surcharge de std :: vector (qui est petit, mais existe toujours).
rxantos

When you're working with your own classes/objects... vous n'avez souvent aucune raison de le faire! Une infime proportion de questions-réponses porte sur les détails de la conception des conteneurs par des codeurs qualifiés. À l' opposé, une proportion déprimant sont de la confusion des débutants qui ne connaissent pas le stdlib existe - ou sont activement donné des missions terribles dans la « programmation » des « cours », où un tuteur exige qu'ils réinventent la roue inutilement - avant même de appris ce qu'est une roue et pourquoi elle fonctionne. En favorisant une allocation plus abstraite, C ++ peut nous sauver du «segfault sans fin avec liste liée» de C; s'il vous plaît, nous allons laisser cela .
underscore_d

" L'utilisation originale de new avec les classes système par l'affiche originale est un peu ridicule. ( int *i; i = new int[9999];? vraiment? int i[9999];est beaucoup plus clair.)" Oui, c'est plus clair, mais pour jouer l'avocat du diable, le type n'est pas nécessairement un mauvais argument. Avec 9999 éléments, je peux imaginer un système embarqué serré n'ayant pas assez de pile pour 9999 éléments: 9999x4 octets est ~ 40 Ko, x8 ~ 80 Ko. De tels systèmes peuvent donc avoir besoin d'utiliser l'allocation dynamique, en supposant qu'ils l'implémentent en utilisant une mémoire alternative. Pourtant, cela ne pouvait peut-être que justifier une allocation dynamique, non new; un vectorserait la vraie solution dans ce cas
underscore_d

D'accord avec @underscore_d - ce n'est pas un si bon exemple. Je n'ajouterais pas 40 000 ou 80 000 octets à ma pile comme ça. Je les répartirais probablement sur le tas (avec std::make_unique<int[]>()bien sûr).
einpoklum

3

Deux raisons:

  1. C'est inutile dans ce cas. Vous compliquez inutilement votre code.
  2. Il alloue de l'espace sur le tas, et cela signifie que vous devez vous en souvenir deleteplus tard, sinon cela entraînera une fuite de mémoire.

2

newest le nouveau goto.

Rappelez-vous pourquoi il gotoest si vilipendé: bien qu'il s'agisse d'un outil puissant et de bas niveau pour le contrôle de flux, les gens l'utilisaient souvent de manière inutilement compliquée, ce qui rendait le code difficile à suivre. De plus, les modèles les plus utiles et les plus faciles à lire étaient codés dans des instructions de programmation structurées (par exemple forou while); l'effet ultime est que le code où gotoest le moyen approprié est plutôt rare, si vous êtes tenté d'écrire goto, vous faites probablement mal les choses (sauf si vous savez vraiment ce que vous faites).

newest similaire - il est souvent utilisé pour rendre les choses inutilement compliquées et plus difficiles à lire, et les modèles d'utilisation les plus utiles peuvent être codés ont été codés dans différentes classes. De plus, si vous avez besoin d'utiliser de nouveaux modèles d'utilisation pour lesquels il n'y a pas déjà de classes standard, vous pouvez écrire vos propres classes qui les encodent!

Je dirais même que newc'est pire que goto, en raison de la nécessité de jumeler newet de deletedéclarations.

Par exemple goto, si vous pensez que vous devez utiliser new, vous faites probablement mal les choses - surtout si vous le faites en dehors de la mise en œuvre d'une classe dont le but dans la vie est d'encapsuler les allocations dynamiques que vous devez faire.


Et j'ajouterais: "Vous n'en avez simplement pas besoin".
einpoklum

1

La raison principale est que les objets sur le tas sont toujours difficiles à utiliser et à gérer que les valeurs simples. Écrire du code facile à lire et à maintenir est toujours la première priorité de tout programmeur sérieux.

Un autre scénario est que la bibliothèque que nous utilisons fournit une sémantique de valeur et rend l'allocation dynamique inutile. Std::stringest un bon exemple.

Pour le code orienté objet, cependant, l'utilisation d'un pointeur - ce qui signifie newque vous devez le créer au préalable - est un must. Afin de simplifier la complexité de la gestion des ressources, nous avons des dizaines d'outils pour la rendre aussi simple que possible, comme des pointeurs intelligents. Le paradigme basé sur les objets ou le paradigme générique suppose une sémantique de valeur et nécessite moins ou pas new, tout comme les affiches énoncées ailleurs.

Les modèles de conception traditionnels, en particulier ceux mentionnés dans le livre du GoF , en utilisent newbeaucoup, car ils sont du code OO typique.


4
C'est une réponse épouvantable . For object oriented code, using a pointer [...] is a must: un non - sens . Si vous dévaluez 'OO' en vous référant uniquement à un petit sous-ensemble, le polymorphisme - aussi un non-sens: les références fonctionnent aussi. [pointer] means use new to create it beforehand: particulièrement absurde : des références ou des pointeurs peuvent être utilisés pour les objets alloués automatiquement et utilisés de manière polymorphe; regarde moi . [typical OO code] use new a lot: peut-être dans un vieux livre, mais qui s'en soucie? Tous newles pointeurs C ++ vaguement modernes évitent / pointent dans la mesure du possible - et ne sont en aucun cas moins OO en le faisant
underscore_d

1

Un point de plus pour toutes les bonnes réponses ci-dessus, cela dépend du type de programmation que vous faites. Développement du noyau sous Windows par exemple -> La pile est sévèrement limitée et vous ne pourrez peut-être pas accepter les défauts de page comme en mode utilisateur.

Dans de tels environnements, les appels d'API nouveaux ou similaires à C sont préférés et même requis.

Bien sûr, ce n'est qu'une exception à la règle.


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.