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


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

Arrête d'utiliser new tellement de. Je ne vois aucune raison que tu utilises nouveau n'importe où   Tu l'as fait. Vous pouvez créer des objets par valeur en C ++ et c'est l'un des   énormes avantages à utiliser la langue. Vous n'avez pas à allouer   tout 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 valeur aussi souvent que possible en C ++, et quelle différence cela fait-il en interne? Ai-je mal interprété la réponse?


750
2018-06-28 00:08


origine


Réponses:


Il existe deux techniques d'allocation de mémoire largement utilisées: l'allocation automatique et l'allocation dynamique. Généralement, il y a 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 parce qu'il vous oblige à 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. Il est très, très rapide car il nécessite une comptabilité minimale et la prochaine adresse à attribuer est implicite.

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

Tas

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

Raisons d'utiliser l'allocation dynamique

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

Deux raisons principales pour 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 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) cela 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 sur la durée de vie d'une variable. Cette technique est appelée RAII et est le point distinctif de C ++. Il "enveloppe" 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 réellement une quantité variable de mémoire. le std::string object alloue de la mémoire en utilisant le tas et le libère dans son destructeur. Dans ce cas, vous avez fait ne pas besoin de gérer manuellement toutes les ressources et encore eu les avantages de l'allocation de mémoire dynamique.

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 allocation de mémoire dynamique inutile. Le programme nécessite plus de dactylographie (!) Et introduit le risque d'oublier de libérer la mémoire. Il le fait sans bénéfice apparent.

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

Fondamentalement, le dernier paragraphe le résume. Utiliser le stockage automatique le plus souvent 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 référencée, il y a d'autres préoccupations. 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 effectivement 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 delete sur la même chaîne deux fois. En utilisant la version modifiée, chaque Line instance va posséder sa propre chaîne exemple, chacun avec sa propre mémoire et les deux seront libérés à la fin du programme.

Autres notes

Utilisation intensive de RAII est considéré comme une bonne 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 somme de ses parties. L'ensemble du mécanisme compose. Il échelles.

Si vous utilisez le Line classe comme un bloc de construction:

 class Table
 {
      Line borders[4];
 };

alors

 int main ()
 {
     Table table;
 }

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


898
2018-06-28 00:47



Parce que la pile est rapide et infaillible

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 fuir n'importe quelle mémoire. Ce commentaire avait l'intention (ou aurait dû) de dire quelque chose comme "utilisez la pile et non le tas".


155
2018-06-28 00:14



C'est compliqué.

Tout d'abord, C ++ n'est pas collecté. Par conséquent, pour chaque nouveau, il doit y avoir une suppression correspondante. Si vous ne parvenez pas à mettre cette suppression, alors 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" lance une exception? Oups: fuite de mémoire. Que se passe-t-il si des problèmes "Faites des choses" return de bonne heure? 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, ils doivent maintenant la supprimer. Et s'ils le passent en argument, la personne qui le reçoit doit-elle le 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 qu'il sera 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 en tant que référence ou const-référence: void SomeFunc(std::string &iCanModifyThis, const std::string &iCantModifyThis). Et ainsi de suite.

Tout 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::string someString(...);
std::string otherString;
otherString = someString;

Il est bien compris que otherString a une copie du Les données 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?


94
2018-06-28 00:17



Objets créés par new doit être finalement deleteDe peur qu'ils ne fuient. Le destructeur ne sera pas appelé, la mémoire ne sera pas libérée, tout le bit. Puisque C ++ n'a aucune collection de place, c'est un problème.

Les objets créés par valeur (par exemple 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 lors du retour de la fonction.

Des pointeurs intelligents comme auto_ptr, shared_ptr résoudre le problème de référence qui pend, mais ils nécessitent une discipline de codage et ont d'autres problèmes (reproductibilité, boucles de référence, etc.).

En outre, dans des scénarios fortement multithread, new est un point de conflit entre les threads; il peut y avoir un impact sur les performances pour une utilisation excessive new. La création d'un objet pile est par définition thread-local, puisque chaque thread a sa propre pile.

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


65
2018-06-28 00:11



  • C ++ n'utilise pas de gestionnaire de mémoire par lui-même. D'autres langages comme C #, Java a garbage collector pour gérer la mémoire
  • C ++ utilisant les routines du système d'exploitation pour allouer la mémoire et trop de nouvelles / 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 relâcher lorsqu'elle n'est pas requise.
  • Une mauvaise gestion de la mémoire peut entraîner des fuites de mémoire et c'est vraiment difficile à suivre. Donc, en utilisant des objets de pile dans le cadre de la fonction est une technique éprouvée
  • L'inconvénient de l'utilisation des objets de pile est, il crée plusieurs copies d'objets lors du retour, passage à des fonctions, etc. Cependant, les compilateurs intelligents sont bien conscients de ces situations et ils ont été bien 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 communément accessibles, des objets de pile (maximum possible) et des techniques comme auto_ptr (objets RAII)
  • La meilleure chose est que, vous avez le contrôle sur la mémoire et la pire chose 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 accidents causés par les corruptions de la mémoire sont les plus méchants et difficiles à tracer.

27
2018-06-28 02:59



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 avec la création d'objets en utilisant le new opérateur. Ce qu'il y a un argument pour cela, c'est 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 quand 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 GC le trouve.

Avais-tu écrit

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

vous voudriez le faire correspondre explicitement avec

     delete foo;
  }

ou mieux encore, allouer 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 amput 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 FileImpl volonté encore être alloué sur la pile.

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

     ~File(){ delete fd ; }

dans la classe aussi; sans cela, vous ferez couler la mémoire du tas, même si vous ne l'avez pas Apparemment allouer sur le tas du tout.


17
2018-06-28 00:11



Je vois que quelques raisons importantes pour faire aussi peu de nouvelles que possible sont manquées:

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

Appel new peut ou non faire que le système d'exploitation alloue 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 new dans vos boucles critiques de temps.

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 appeler ainsi new entraînera votre thread à acquérir un verrou de mutex implicite. Si vous appelez constamment new à partir de nombreux threads, vous êtes en train de sérialiser vos threads (je l'ai fait avec 32 processeurs, chacun touchant new pour obtenir quelques centaines de bytes chacun, aïe! c'était un p.i.t.a royal. déboguer)

Le reste, comme la lenteur, la fragmentation, l'erreur, etc. ont déjà été mentionnés par d'autres réponses.


16
2018-02-12 17:57



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 détruire sur l'objet que vous avez placé sur le tas avec de nouveaux. 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!


13
2018-06-28 00:10



new() ne devrait pas être utilisé comme peu possible. Il devrait être utilisé comme soigneusement possible. Et il devrait être utilisé aussi souvent que nécessaire dicté par 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 d'utiliser new(), avec les associés delete() et vérification des pointeurs NULL. Dans le cas où vous avez beaucoup d'objets éphémères, l'allocation sur la pile 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, alors new() est la bonne réponse. Assurez-vous juste que vous faites attention à quand et comment vous appelez delete() et les possibilités des pointeurs NULL, en utilisant des objets supprimés et tous les autres gotchas qui viennent avec l'utilisation de pointeurs.


13
2018-06-28 00:38