web-dev-qa-db-fra.com

Pourquoi les programmeurs C ++ devraient-ils minimiser l'utilisation de "nouveaux"?

Je suis tombé par hasard sur la question de débordement de pile Fuite de mémoire avec std :: string en utilisant std :: list <std :: string> , et n des commentaires dit ceci:

Arrêtez tellement d'utiliser new. Je ne vois aucune raison pour laquelle vous avez utilisé le nouveau où que vous soyez. Vous pouvez créer des objets par valeur en C++ et c'est l'un des avantages majeurs de l'utilisation du langage. Vous n'êtes pas obligé de 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 objets devraient-ils être créés par la valeur en C++ aussi souvent que possible et quelle différence cela fait-il en interne? Ai-je mal interprété la réponse?

821
bitgarden

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. Cela est possible car vous devez libérer la mémoire dans l'ordre inverse (premier entré, dernier sorti: 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 il nécessite peu de comptabilité et l'adresse suivante à attribuer est implicite.

En C++, cela s'appelle le stockage automatique car le stockage est réclamé 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ù des 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 point de libération implicite, vous devez libérer la mémoire manuellement, à l'aide de delete ou delete[] (free dans C). Cependant, l'absence de point de libération implicite est la clé de la flexibilité du segment de mémoire.

Raisons d'utiliser l'allocation dynamique

Même si l'utilisation du segment de mémoire est plus lente et conduit potentiellement à des fuites de mémoire ou à une fragmentation de la mémoire, il existe de très bons cas d'utilisation pour l'allocation dynamique, car ils sont moins limités.

Deux raisons principales d'utiliser l'allocation dynamique:

  • 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 tant que vous n'avez pas exécuté le programme.

  • Vous voulez allouer de la mémoire qui persistera après avoir quitté le bloc actuel. Par exemple, vous pouvez é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 existe 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 sur la durée de vie d'une variable. Cette technique s'appelle RAII et est le point distinctif de C++. Il "encapsule" les ressources dans des objets. std::string est un exemple parfait. Cet extrait:

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

alloue en réalité une quantité variable de mémoire. L'objet std::string alloue de la mémoire à l'aide du segment de mémoire et la libère dans son destructeur. Dans ce cas, vous n'avez pas besoin de gérer manuellement les ressources tout en bénéficiant 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 dactylographie (!) Et introduit le risque d'oubli pour désallouer la mémoire. Cela ne présente aucun avantage apparent.

Pourquoi devriez-vous utiliser le stockage automatique aussi souvent que possible

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

  • plus rapide à taper;
  • plus rapide lorsqu'il est exécuté;
  • moins sujet aux fuites de mémoire/ressources.

Points bonus

Dans la question mentionnée, il y a 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 réalité 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::string définit correctement un constructeur de copie. Considérez le programme suivant:

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

En utilisant la version originale, ce programme va probablement planter, car il utilise deux fois delete sur la même chaîne. En utilisant la version modifiée, chaque instance de Line possédera sa propre instance de chaîne , chacune avec sa propre mémoire et les deux seront libéré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 susmentionnées. Cependant, il existe un avantage supplémentaire qui n’est pas immédiatement évident. En gros, c'est mieux que la somme de ses parties. L'ensemble du mécanisme compose . Ça pèse.

Si vous utilisez la classe Line comme bloc de construction:

 class Table
 {
      Line borders[4];
 };

Ensuite

 int main ()
 {
     Table table;
 }

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

973
André Caron

Parce que la pile est plus rapide et étanche

En C++, il ne faut qu'une seule instruction pour allouer de l'espace - sur la pile - à chaque objet de la portée locale d'une fonction donnée, et il est impossible de perdre de la mémoire. Ce commentaire voulait (ou aurait dû avoir l'intention) de dire quelque chose comme "utilise la pile et non le tas".

166
DigitalRoss

La raison est compliquée.

Premièrement, 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 les problèmes "Do stuff" return tôt? Oups: fuite de mémoire.

Et ceci est pour le cas le plus simple. Si vous retournez cette chaîne à quelqu'un, il doit maintenant la supprimer. Et s’ils en font un argument, la personne qui le reçoit doit-elle l’effacer? 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 qu'il sera hors de portée. Vous pouvez même renvoyer l'objet, transférant ainsi son contenu à la fonction appelante. Vous pouvez transmettre l'objet à des fonctions (généralement sous la forme d'une référence ou d'une référence constante: void SomeFunc(std::string &iCanModifyThis, const std::string &iCantModifyThis). Et ainsi de suite.

Tous sans new et delete. Il n'est pas question de savoir qui est propriétaire 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 otherString a une copie du data de someString. Ce n'est pas un pointeur; c'est un objet séparé. Ils peuvent avoir le même contenu, mais vous pouvez en changer un sans affecter l’autre:

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

Voir l'idée?

102
Nicol Bolas

Les objets créés par new doivent être finalement deleted sinon ils risquent de fuir. Le destructeur ne sera pas appelé, la mémoire ne sera pas libérée, le bit entier. Comme C++ n’a pas de récupération de place, c’est un problème.

Les objets créés par une valeur (c'est-à-dire sur une 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 libérée automatiquement au retour de la fonction.

Les pointeurs intelligents tels que unique_ptr, shared_ptr résolvent le problème de référence, mais ils nécessitent une discipline de codage et présentent d'autres problèmes potentiels (possibilité de copie, boucles de référence, etc.).

De plus, dans les scénarios fortement multithread, new est un point de conflit entre les threads; une surutilisation de new peut avoir un impact sur les performances. La création d’un objet pile est par définition un thread local, car chaque thread a sa propre pile.

L'inconvénient des objets de valeur est qu'ils meurent une fois que la fonction Host est revenue - vous ne pouvez pas renvoyer une référence à ceux-ci à l'appelant, uniquement en les copiant, en les retournant ou en les déplaçant par valeur.

73
Seva Alekseyev
  • C++ n'emploie 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 de la mémoire et trop de nouvelles opérations de suppression/suppression pourraient fragmenter la mémoire disponible.
  • Quelle que soit l'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 pourrait entraîner des fuites de mémoire et il est très difficile à suivre. Donc, utiliser des objets de pile dans le cadre de la fonction est une technique éprouvée
  • L’inconvénient d’utiliser des objets de pile est qu’il crée plusieurs copies d’objets lors du retour, du passage à des fonctions, etc. Cependant, les compilateurs intelligents sont bien conscients de ces situations et ont été optimisés pour la performance.
  • 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 nous nous appuyons principalement sur des pointeurs, objets de pile (le plus possible) et des techniques couramment accessibles, comme auto_ptr (objets RAII).
  • Le mieux, c’est que vous maîtrisiez la mémoire, et le pire, c’est que vous n’auriez aucun contrôle sur la mémoire si nous utilisons une gestion de mémoire inappropriée pour l’application. Les crashs dus à des corruptions de mémoire sont les plus méchants et difficiles à tracer.
29
sarat

Je vois qu'il manque quelques raisons importantes de faire le moins de choses possible:

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

L'appel de new peut amener le système d'exploitation à attribuer 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 jeu/physique), vous devez éviter new dans vos boucles critiques.

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

Oui, vous m'avez entendu. Votre système d'exploitation doit s'assurer de la cohérence de vos tables de pages. Par conséquent, si vous appelez new, votre thread obtiendra un verrou mutex implicite. Si vous appelez toujours new à partir de nombreux threads, vous sérialisez réellement vos threads (je l'ai déjà fait avec 32 processeurs, chacun appuyant sur new pour obtenir quelques centaines d'octets chacun, aïe! pita royal à déboguer)

Les autres réponses, telles que lente, fragmentation, risque d'erreur, etc. ont déjà été mentionnées.

23
Emily L.

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’envelopper des objets dans des pointeurs intelligents:

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

Ce code est dangereux car aucune garantie que soit shared_ptr soit construit avant soit T1 ou T2. Par conséquent, si l'un de new T1() ou new T2() échoue une fois que l'autre réussit, le premier objet subira une fuite car aucun shared_ptr n'existe pour le détruire et le désallouer.

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 veillant à ce que chaque appel à new() soit immédiatement suivi de la construction du pointeur intelligent correspondant, sans autre opération entre les deux. Cela implique que, lorsque la seconde new() est appelée, il est garanti que le premier objet a déjà été encapsulé dans son pointeur intelligent, ce qui évite toute fuite en cas d'exception.

Une explication plus détaillée de la nouvelle commande d'évaluation introduite par C++ 17 a été fournie par Barry dans une autre réponse .

Merci à @ Rémy Lebea pour avoir signalé qu'il s'agit toujours d'un problème sous C++ 17 (encore que moins): le constructeur shared_ptr peut échouer dans l'allocation de son bloc de contrôle et de sa projection, auquel cas le pointeur qui lui est transmis n'est pas supprimé.

Solution: utilisez make_shared.

18
Mehrdad

Dans une large mesure, c'est quelqu'un qui élève ses propres faiblesses en règle générale. Il n'y a rien de mal en soi à créer des objets à l'aide de l'opérateur new. Certains prétendent 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.

Le moyen le plus simple de le faire est de créer l'objet dans la mémoire de stockage automatique, afin que C++ sache le détruire lorsqu'il sort de la portée:

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

    // do things

 }

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

Avais-tu écrit

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

vous voudriez le faire correspondre explicitement avec

     delete foo;
  }

ou mieux encore, affectez votre File * comme un "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 new vous 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, par exemple un pointeur, est certainement allouée sur la pile. Cependant, considérez si la mise en oeuvre de File est quelque chose comme

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

alors FileImpl sera toujours toujours alloué sur la pile.

Et oui, vous feriez mieux de vous assurer d'avoir

     ~File(){ delete fd ; }

dans la classe aussi bien; sans cela, vous perdrez de la mémoire même si vous n'aviez pas apparemment alloué sur le tas.

17
Charlie Martin

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

L'attribution 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 d'utiliser new(), avec le delete() associé et la vérification des pointeurs NULL. Dans le cas où vous avez beaucoup d'objets de courte durée sur la pile, cela devrait réduire les problèmes de fragmentation de tas.

Cependant, 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. 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 des pointeurs.

15
Andrew Edgecombe

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 le potentiel de fuites de mémoire. Les objets placés sur la pile ne sont pas sujets aux fuites de mémoire!

13
Tim

Une des raisons notables pour éviter une utilisation excessive du segment de mémoire est la performance, notamment celle du mécanisme de gestion de la mémoire par défaut utilisé par C++. Bien que l'allocation puisse être assez rapide dans le cas trivial, faire beaucoup de new et delete sur des objets de taille non uniforme sans ordre strict conduit 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 pools de mémoire a été créé pour être résolu, ce qui permet d’atténuer les inconvénients inhérents aux implémentations de tas classiques, tout en vous permettant d’utiliser le tas si nécessaire.

Mieux encore, évitez le problème. Si vous pouvez le mettre sur la pile, faites-le.

11
tylerl

J'ai tendance à être en désaccord avec l'idée d'utiliser le nouveau "trop". Bien que l’affichage original utilise de nouvelles classes système, c’est un peu ridicule. (int *i; i = new int[9999];? Vraiment? int i[9999]; est beaucoup plus clair.) Je pense que est ce qui attira la chèvre du commentateur.

Lorsque vous travaillez avec des objets système, il est rare très que vous ayez besoin de plus d'une référence au même objet. Tant que la valeur est la même, c'est tout ce qui compte. Et les objets système ne prennent 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 devraient être conçues pour prendre en compte cette gestion de la mémoire (si elles sont bien écrites). Dans ces cas, (tous sauf une ou deux des nouvelles dans son code), le nouveau 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 à 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 des listes chaînées, des dictionnaires et des graphiques, où plusieurs variables doivent non seulement avoir la même valeur, mais également la même objet en mémoire. Cependant, la classe Line n'a aucune de ces exigences. Le code de l'affiche originale n'a donc absolument aucun besoin de new.

10
Chris Hayes

Je pense que l’affiche voulait dire You do not have to allocate everything on theheap plutô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 peu coûteux de l'allocation de pile, plutôt que de l'allocation basée sur le tas, qui nécessite un travail considérable de la part de l'allocateur, et ajoute de la verbosité, car il faut gérer les données allouées sur le tas.

10
Khaled Nassar

Deux raisons:

  1. C'est inutile dans ce cas. Vous compliquez inutilement votre code.
  2. Cela alloue de l'espace sur le tas et cela signifie que vous devez vous souvenir de delete plus tard, sinon cela provoquera une fuite de mémoire.
3
Dan

new est le nouveau goto.

Rappelez-vous pourquoi goto est si vénéré: bien qu'il s'agisse d'un puissant outil de contrôle de flux de bas niveau, il était souvent utilisé de manière inutilement compliquée rendant 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 le moyen approprié est plutôt rare. Si vous êtes tenté d’écrire goto, vous faites probablement mal les choses (à moins que vous vraiment savez ce que vous faites).

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

Je dirais même que new est pire que goto, en raison de la nécessité d'associer new et delete déclarations.

Comme goto, si vous pensez avoir besoin d'utiliser new, vous faites probablement des choses mal, surtout si vous le faites en dehors de la mise en œuvre d'une classe dont le but dans la vie est d'encapsuler toute dynamique. les allocations que vous devez faire.

2
Hurkyl

La raison principale est que les objets sur tas sont toujours difficiles à utiliser et à gérer que de simples valeurs. Écrire un 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 dynamique. Std::string est un bon exemple.

Toutefois, pour un code orienté objet, il est indispensable d’utiliser un pointeur (c’est-à-dire utiliser new pour le créer à l’avance). Afin de simplifier la complexité de la gestion des ressources, nous disposons de dizaines d'outils pour le rendre aussi simple que possible, tels que les pointeurs intelligents. Le paradigme basé sur les objets ou le paradigme générique suppose une sémantique de valeur et nécessite peu ou pas de new, tout comme l'ont indiqué les affiches ailleurs.

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

1
bingfeng zhao

Un point supplémentaire à toutes les réponses correctes ci-dessus, cela dépend de quel type de programmation vous faites. Le noyau se développant sous Windows par exemple -> La pile est sévèrement limitée et vous ne pourrez peut-être pas supporter les erreurs de page comme en mode utilisateur.

Dans de tels environnements, les appels d'API nouveaux ou de type C sont préférables et même requis.

Bien entendu, il ne s'agit que d'une exception à la règle.

1
Michael Chourdakis