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

Je suis tombé sur Stack Overflow question fuite de mémoire avec std :: ssortingng en utilisant std :: list , et l’ un des commentaires dit ceci:

Arrêtez d’utiliser autant de choses. Je ne vois aucune raison pour laquelle vous avez utilisé de nouveaux produits. Vous pouvez créer des objects en valeur en C ++ et c’est l’un des grands 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 suis pas vraiment sûr de ce qu’il veut dire par là. Pourquoi les objects 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?

Il existe deux techniques d’allocation de mémoire très répandues: l’allocation automatique et l’allocation dynamic. Généralement, il y a une région de mémoire correspondante pour chacun: la stack et le tas.

Emstackr

La stack alloue toujours de la mémoire de manière séquentielle. Cela peut être fait car il vous faut libérer la mémoire dans l’ordre inverse (First-In, Last-Out: FILO). C’est la technique d’allocation de mémoire pour les variables locales dans de nombreux langages de programmation. C’est très, très rapide car cela nécessite une comptabilité minimale et la prochaine adresse à atsortingbuer est implicite.

En C ++, cela s’appelle le stockage automatique car le stockage est réclamé automatiquement à la fin de la scope. Dès que l’exécution du bloc de code en cours (délimité par {} ) est terminée, la mémoire de toutes les variables de ce bloc est automatiquement collectée. C’est aussi 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. Comme il n’y a pas de sharepoint sortie implicite, vous devez libérer la mémoire manuellement en utilisant delete ou delete[] ( free dans C). Cependant, l’absence de sharepoint sortie implicite est la clé de la flexibilité du tas.

Raisons d’utiliser l’allocation dynamic

Même si l’utilisation du tas est plus lente et conduit potentiellement à des memory leaks ou à une fragmentation de la mémoire, il existe des cas d’utilisation parfaitement appropriés pour l’allocation dynamic, car elle est moins limitée.

Deux raisons principales pour utiliser l’allocation dynamic:

  • Vous ne savez pas combien de mémoire vous avez besoin au moment de la compilation. Par exemple, lors de la lecture d’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 en cours. Par exemple, vous souhaiterez peut-être écrire une ssortingng readfile(ssortingng path) fonctions ssortingng readfile(ssortingng path) qui renvoie le contenu d’un fichier. Dans ce cas, même si la stack 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 dynamic 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 constitue le point distinctif du C ++. Il “enveloppe” des ressources dans des objects. std::ssortingng est un exemple parfait. Cet extrait:

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

alloue effectivement une quantité variable de mémoire. L’object std::ssortingng alloue de la mémoire en utilisant le tas et le libère dans son destructeur. Dans ce cas, vous n’avez pas besoin de gérer manuellement les ressources et bénéficiez toujours des avantages de l’allocation dynamic de mémoire.

En particulier, cela implique que dans cet extrait:

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

il y a une allocation de mémoire dynamic inutile. Le programme nécessite plus de frappe (!) Et introduit le risque d’oublier de désallouer la mémoire. Cela se fait sans bénéfice apparent.

Pourquoi vous devriez 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 quand courir;
  • moins sujettes aux memory leaks / de ressources.

Points bonus

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

 class Line { public: Line(); ~Line(); std::ssortingng* mSsortingng; }; Line::Line() { mSsortingng = new std::ssortingng("foo_bar"); } Line::~Line() { delete mSsortingng; } 

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

 class Line { public: Line(); std::ssortingng mSsortingng; }; Line::Line() { mSsortingng = "foo_bar"; // note: there is a cleaner way to write this. } 

La raison en est que std::ssortingng définit correctement un constructeur de copie. Considérons le programme suivant:

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

En utilisant la version originale, ce programme va probablement tomber en panne, car il utilise deux fois la même delete sur la même chaîne. En utilisant la version modifiée, chaque instance Line 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 y a un avantage supplémentaire qui n’est pas immédiatement évident. Fondamentalement, c’est mieux que la sum de ses parties. Tout le mécanisme compose . Ça balance

Si vous utilisez la classe de Line comme bloc de construction:

  class Table { Line borders[4]; }; 

alors

  int main () { Table table; } 

alloue quatre instances std::ssortingng , quatre instances Line , une instance Table et tout le contenu de la chaîne et tout est libéré automatiquement .

Parce que la stack est rapide et infaillible

En C ++, il suffit d’une seule instruction pour allouer de l’espace – sur la stack – pour chaque object de scope locale dans une fonction donnée, et il est impossible de fuir cette mémoire. Ce commentaire visait (ou aurait dû vouloir) dire quelque chose comme “utiliser la stack et non le tas”.

C’est compliqué.

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::ssortingng *someSsortingng = new std::ssortingng(...); //Do stuff delete someSsortingng; 

C’est simple. Mais que se passe-t-il si “Do stuff” jette une exception? Oops: fuite de mémoire. Que se passe-t-il si les émissions “Do stuff” return tôt? Oops: fuite de mémoire.

Et c’est pour le cas le plus simple . Si vous retournez cette chaîne à quelqu’un, il doit maintenant la supprimer. Et si elles le passent en argument, la personne qui la reçoit doit-elle la supprimer? Quand doivent-ils le supprimer?

Ou, vous pouvez simplement faire ceci:

 std::ssortingng someSsortingng(...); //Do stuff 

Pas de delete L’object a été créé sur la “stack” et il sera détruit dès qu’il sera hors de scope. Vous pouvez même retourner l’object, transférant ainsi son contenu à la fonction appelante. Vous pouvez passer l’object à des fonctions (généralement en tant que référence ou const-reference: void SomeFunc(std::ssortingng &iCanModifyThis, const std::ssortingng &iCantModifyThis) . Et ainsi de suite.

Tous sans new et delete . Il n’est pas question de savoir à qui appartient la mémoire ou qui est responsable de la supprimer. Si tu fais:

 std::ssortingng someSsortingng(...); std::ssortingng otherSsortingng; otherSsortingng = someSsortingng; 

Il est entendu que otherSsortingng a une copie des données de someSsortingng . Ce n’est pas un pointeur; c’est un object séparé. Ils peuvent avoir le même contenu, mais vous pouvez en changer un sans affecter l’autre:

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

Voir l’idée?

Les objects créés par new doivent éventuellement être delete fuir. Le destructeur ne sera pas appelé, la mémoire ne sera pas libérée, le bit entier. Puisque C ++ n’a pas de récupération de mémoire, c’est un problème.

Les objects créés par valeur (c’est-à-dire sur la stack) meurent automatiquement lorsqu’ils sortent du cadre. L’appel de destructeur est inséré par le compilateur et la mémoire est automatiquement libérée lors du retour de la fonction.

Les pointeurs intelligents tels que auto_ptr , shared_ptr résolvent le problème de référence en suspens, mais ils requièrent une discipline de codage et d’autres problèmes (reproductibilité, boucles de référence, etc.).

En outre, dans les scénarios fortement multithread, new est un sharepoint discorde entre les threads; il peut y avoir un impact sur la performance pour la surutilisation. La création d’objects de stack est par définition thread-local, puisque chaque thread a sa propre stack.

L’inconvénient des objects de valeur est qu’ils meurent une fois que la fonction hôte retourne – vous ne pouvez pas transmettre une référence à ceux-ci à l’appelant, uniquement en copiant ou en retournant par valeur.

  • C ++ n’utilise aucun gestionnaire de mémoire. Autres langages comme C #, Java dispose d’un ramasse-miettes pour gérer la mémoire
  • C ++ utilisant des routines de système d’exploitation pour allouer la mémoire et trop de nouveau / supprimer pourrait 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 si elle n’est pas requirejse.
  • Une mauvaise gestion de la mémoire peut entraîner des memory leaks et il est très difficile de suivre le chemin. Donc, l’utilisation d’objects de stack dans le cadre de la fonction est une technique éprouvée
  • L’inconvénient de l’utilisation d’objects de stack est qu’il crée plusieurs copies d’objects lors du retour, en passant aux fonctions, etc. Cependant, les compilateurs intelligents sont bien au courant de ces situations et ils ont été 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 publication est toujours une question et la plupart du temps, nous nous appuyons sur des pointeurs, des objects de stack (maximum possible) et des techniques telles que auto_ptr (objects 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 gestion de mémoire incorrecte pour l’application. Les pannes causées par des corruptions de mémoire sont les plus mauvaises et les plus difficiles à tracer.

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 à créer des objects avec le new opérateur. Il y a un argument pour lequel vous devez le faire avec une certaine discipline: si vous créez un object, vous devez vous assurer qu’il sera détruit.

La manière la plus simple de le faire est de créer l’object dans un stockage automatique, afin que C ++ sache le détruire quand il sort de son champ d’application:

  { File foo = File("foo.dat"); // do things } 

Maintenant, observez que lorsque vous tombez de ce bloc après l’accolade, foo est hors de scope. 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 voudriez le faire correspondre explicitement avec

  delete foo; } 

ou mieux encore, allouez votre File * tant que “pointeur intelligent”. Si vous ne faites pas attention à cela, cela peut entraîner des fuites.

La réponse elle-même fait l’hypothèse erronée que si vous n’utilisez pas de new vous ne les 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 stack. Cependant, considérez si l’implémentation de File est quelque chose comme

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

alors FileImpl sera toujours alloué sur la stack.

Et oui, il vaut mieux être sûr d’avoir

  ~File(){ delete fd ; } 

dans la classe aussi; sans elle, vous perdrez de la mémoire dans le tas même si, apparemment, vous ne l’allouiez pas du tout.

Je constate que quelques raisons importantes pour faire le moins de nouvelles possible sont manquées:

L’opérateur new a un temps d’exécution non déterministe

En appelant new le système d’exploitation peut ou non allouer une nouvelle page physique à votre processus, ce qui 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 soaps pas. Si votre programme a besoin d’un temps d’exécution cohérent et prévisible (comme dans un système temps réel ou une simulation de jeu / physique), vous devez éviter les new boucles critiques de votre temps.

L’opérateur new est une synchronisation de thread implicite

Oui, vous m’avez entendu, votre système d’exploitation doit s’assurer que vos tables de pages sont cohérentes et qu’en tant que tel, l’appel de votre thread entraînera l’acquisition d’un verrou de mutex implicite. Si vous appelez régulièrement de new threads à partir de nombreux threads, vous sérialisez en fait vos threads (j’ai fait cela avec 32 processeurs, chacun utilisant de new processeurs pour obtenir quelques centaines d’octets, ouch! C’était un pita royal à déboguer)

Les autres, comme la lenteur, la fragmentation, les erreurs, etc., ont déjà été mentionnées par d’autres réponses.

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

 Class var; 

il est placé sur la stack.

Vous devrez toujours appeler destroy sur l’object que vous avez placé sur le tas avec new. Cela ouvre la possibilité de memory leaks. Les objects placés sur la stack ne sont pas sujets à des memory leaks!

new() ne doit pas être utilisé aussi peu que possible. Il doit être utilisé aussi soigneusement que possible. Et il faut l’utiliser aussi souvent que nécessaire selon le pragmatisme.

L’atsortingbution d’objects sur la stack, en s’appuyant sur leur destruction implicite, est un modèle simple. Si la scope requirejse d’un object correspond à ce modèle, il n’est pas nécessaire d’utiliser new() , avec le point associé delete() et la vérification des pointeurs NULL. Dans le cas où vous avez beaucoup d’objects éphémères, l’allocation sur la stack devrait réduire les problèmes de fragmentation du tas.

Cependant, si la durée de vie de votre object doit dépasser l’étendue actuelle, new() est la bonne réponse. Assurez-vous de faire attention au moment et à la manière dont vous appelez delete() et aux possibilités des pointeurs NULL, en utilisant des objects supprimés et tous les autres pièges associés à l’utilisation de pointeurs.

Pre-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 de placer des objects dans des pointeurs intelligents:

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

Ce code est dangereux car rien ne garantit que soit shared_ptr soit construit avant T1 ou T2 . Par conséquent, si l’un des new T1() ou new T2() échoue après que l’autre ait réussi, le premier object sera divulgué car aucun shared_ptr n’existe pour le détruire et le libérer.

Solution: utilisez 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 dans entre. Cela implique que, au moment où le second new() est appelé, il est garanti que le premier object a déjà été enveloppé dans son pointeur intelligent, empêchant ainsi toute fuite en cas d’exception.

Une explication plus détaillée du nouvel ordre d’évaluation introduit par C ++ 17 a été fournie par Barry dans une autre réponse .

Je pense que l’affiche voulait dire que You do not have to allocate everything on the heap plutôt que sur la stack .

Fondamentalement, les objects sont alloués sur la stack (si la taille de l’object le permet, bien sûr) en raison du coût peu élevé de l’allocation de stack, plutôt que de l’allocation basée sur le tas qui implique un certain travail de l’allocateur. gérer les données allouées sur le tas.

J’ai tendance à être en désaccord avec l’idée d’utiliser un nouveau “trop”. Bien que l’utilisation de nouvelles 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 faisait boucer le commentateur.

Lorsque vous travaillez avec des objects système, il est très rare que vous ayez besoin de plus d’une référence au même object. Tant que la valeur est la même, c’est tout ce qui compte. Et les objects système ne prennent généralement pas beaucoup de place 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-là (toutes sauf une ou deux nouvelles dans son code), la nouveauté est pratiquement inutile et ne sert qu’à introduire des confusions et un potentiel de bogues.

Lorsque vous travaillez avec vos propres classes / objects (par exemple, la classe de ligne de l’affiche originale), vous devez alors commencer à réfléchir 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 – cela permet des constructions telles que les listes liées, les dictionnaires et les graphiques, où plusieurs variables doivent non seulement avoir la même valeur, mais référencer exactement le même object en mémoire. Cependant, la classe de ligne n’a aucune de ces exigences. Le code de l’affiche originale n’a donc absolument aucun besoin de new .

Une raison notable pour éviter de trop utiliser le tas est la performance – impliquant spécifiquement les performances du mécanisme de gestion de la mémoire par défaut utilisé par C ++. Bien que l’allocation puisse être assez rapide dans un cas sortingvial, faire beaucoup de new et de delete objects de taille non uniforme sans ordre ssortingct entraîne non seulement une fragmentation de la mémoire, mais complique aussi l’algorithme d’allocation. .

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

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

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 rappeler de le delete plus tard ou cela entraînera une fuite de mémoire.

La raison principale est que les objects 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 inutile l’allocation dynamic. Std::ssortingng est un bon exemple.

Toutefois, pour un code orienté object, utiliser un pointeur – ce qui signifie utiliser new pour 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 le rendre aussi simple que possible, comme des pointeurs intelligents. Le paradigme basé sur l’object ou le paradigme générique suppose une sémantique de la valeur et exige peu ou pas de new , comme l’indiquent les affiches ailleurs.

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

new est le nouveau goto .

Rappelez-vous pourquoi goto est tellement méchant: bien qu’il s’agisse d’un outil puissant et de bas niveau pour le contrôle de stream, les gens l’utilisaient souvent de manière inutilement compliquée qui rendait le code difficile à suivre. En outre, les modèles les plus utiles et les plus faciles à lire ont été codés dans des instructions de programmation structurées (par exemple for ou while ); L’effet ultime est que le code où goto est la méthode appropriée 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).

new is similar — it is often used to make things unnecessarily complicated and harder to read, and the most useful usage patterns can be encoded have been encoded into various classes. Furthermore, if you need to use any new usage patterns for which there aren’t already standard classes, you can write your own classes that encode them!

I would even argue that new is worse than goto , due to the need to pair new and delete statements.

Like goto , if you ever think you need to use new , you are probably doing things badly — especially if you are doing so outside of the implementation of a class whose purpose in life is to encapsulate whatever dynamic allocations you need to do.

new allocates objects on the heap. Otherwise, objects are allocated on the stack. Look up the difference between the two .