Utilisation correcte de la stack et du tas en C ++?

J’ai programmé pendant un moment mais c’est surtout Java et C #. Je n’ai jamais eu à gérer moi-même ma mémoire. J’ai récemment commencé à programmer en C ++ et je suis un peu confus quand je dois stocker des choses sur la stack et quand les stocker sur le tas.

Je crois savoir que les variables auxquelles on accède très fréquemment doivent être stockées dans la stack et les objects, les variables rarement utilisées et que les grandes structures de données doivent toutes être stockées sur le tas. Est-ce correct ou suis-je incorrect?

Non, la différence entre la stack et le tas n’est pas la performance. C’est la durée de vie: toute variable locale à l’intérieur d’une fonction (tout ce que vous faites n’est pas malloc (ou nouveau)). Il disparaît lorsque vous revenez de la fonction. Si vous voulez que quelque chose vive plus longtemps que la fonction qui l’a déclaré, vous devez l’allouer sur le tas.

class Thingy; Thingy* foo( ) { int a; // this int lives on the stack Thingy B; // this thingy lives on the stack and will be deleted when we return from foo Thingy *pointerToB = &B; // this points to an address on the stack Thingy *pointerToC = new Thingy(); // this makes a Thingy on the heap. // pointerToC contains its address. // this is safe: C lives on the heap and outlives foo(). // Whoever you pass this to must remember to delete it! return pointerToC; // this is NOT SAFE: B lives on the stack and will be deleted when foo() returns. // whoever uses this returned pointer will probably cause a crash! return pointerToB; } 

Pour mieux comprendre ce qu’est la stack, lancez-la de l’autre côté – plutôt que d’essayer de comprendre ce que fait la stack en termes de langage de haut niveau, recherchez la “stack d’appels” et la “convention d’appel” et voyez ce que la machine fonctionne vraiment lorsque vous appelez une fonction. La mémoire informatique n’est qu’une série d’adresses. “tas” et “stack” sont des inventions du compilateur.

Je dirais:

Rangez-le sur la stack, si vous le pouvez.

Rangez-le sur le tas, si vous en avez besoin.

Par conséquent, préférez la stack au tas. Quelques raisons possibles pour lesquelles vous ne pouvez pas stocker quelque chose sur la stack sont:

  • C’est trop gros – sur les programmes multithread sous un système d’exploitation 32 bits, la stack a une taille petite et fixe (au moins au moment de la création du thread) (généralement quelques Mo. Cela vous permet de créer beaucoup de threads sans adresse épuisante) espace.Pour les programmes 64 bits ou les threads simples (Linux quand même), ce n’est pas un problème majeur: sous Linux 32 bits, les programmes à thread unique utilisent généralement des stacks dynamics qui peuvent continuer à se développer jusqu’au bout du tas.
  • Vous devez y accéder en dehors du cadre de la stack d’origine – c’est vraiment la raison principale.

Il est possible, avec des compilateurs sensibles, d’allouer des objects de taille non fixe sur le tas (généralement des tableaux dont la taille n’est pas connue au moment de la compilation).

C’est plus subtil que ne le suggèrent les autres réponses. Il n’y a pas de division absolue entre les données de la stack et celles du tas en fonction de la manière dont vous le déclarez. Par exemple:

 std::vector v(10); 

Dans le corps d’une fonction, qui déclare un vector (tableau dynamic) de dix entiers sur la stack. Mais le stockage géré par le vector n’est pas sur la stack.

Ah, mais (les autres réponses suggèrent) la durée de vie de ce stockage est limitée par la durée de vie du vector lui vector même, qui ici est basée sur la stack, donc cela ne change rien à la façon dont il est implémenté. object avec sémantique de valeur.

Pas si. Supposons que la fonction était:

 void GetSomeNumbers(std::vector &result) { std::vector v(10); // fill v with numbers result.swap(v); } 

Ainsi, tout ce qui a une fonction d’ swap (et n’importe quel type de valeur complexe doit en avoir un) peut servir de référence à certaines données de segment, sous un système qui garantit un propriétaire unique de ces données.

Par conséquent, l’approche C ++ moderne consiste à ne jamais stocker l’adresse de données de tas dans des variables de pointeur local nues. Toutes les allocations de tas doivent être cachées dans les classes.

Si vous faites cela, vous pouvez penser à toutes les variables de votre programme comme s’il s’agissait de types de valeurs simples et oublier complètement le tas (sauf lors de l’écriture d’une nouvelle classe de wrapper de type valeur pour certaines données de tas, ce qui devrait être inhabituel) .

Vous devez simplement conserver un peu de connaissances pour vous aider à optimiser: dans la mesure du possible, au lieu d’affecter une variable à une autre comme ceci:

 a = b; 

échangez-les comme ceci:

 a.swap(b); 

parce que c’est beaucoup plus rapide et qu’il ne jette pas d’exceptions. La seule exigence est que vous n’avez pas besoin de b pour continuer à contenir la même valeur (il va plutôt obtenir a valeur qui sera mise à la poubelle dans a = b ).

L’inconvénient est que cette approche vous oblige à renvoyer des valeurs depuis des fonctions via des parameters de sortie au lieu de la valeur de retour réelle. Mais ils corrigent cela dans C ++ 0x avec des références rvalue .

Dans les situations les plus compliquées, vous pouvez utiliser cette idée à l’extrême et utiliser une classe de pointeur intelligente telle que shared_ptr qui est déjà dans tr1. (Bien que je dirais que si vous semblez en avoir besoin, vous avez peut-être dépassé le niveau d’applicabilité de Standard C ++.)

Vous devez également stocker un élément sur le tas s’il doit être utilisé en dehors du cadre de la fonction dans laquelle il est créé. Un idiome utilisé avec des objects de stack est appelé RAII – cela implique l’utilisation de l’object basé sur la stack comme enveloppe pour une ressource, lorsque l’object est détruit, la ressource serait nettoyée. Les objects basés sur la stack sont plus faciles à suivre lorsque vous émettez des exceptions – vous n’avez pas besoin de vous soucier de supprimer un object basé sur un segment de mémoire dans un gestionnaire d’exceptions. C’est pourquoi les pointeurs bruts ne sont normalement pas utilisés dans le C ++ moderne. Vous utiliseriez un pointeur intelligent qui pourrait être un wrapper basé sur une stack pour un pointeur brut vers un object basé sur un tas.

Pour append aux autres réponses, il peut également s’agir de performances, du moins un peu. Vous ne devez pas vous inquiéter à moins que cela ne vous concerne, mais:

L’allocation dans le tas nécessite de trouver un suivi d’un bloc de mémoire, ce qui n’est pas une opération à temps constant (et prend quelques cycles et une surcharge). Cela peut ralentir lorsque la mémoire est fragmentée et / ou que vous utilisez presque 100% de votre espace d’adressage. D’autre part, les allocations de stack sont des opérations à temps constant, essentiellement “gratuites”.

Une autre chose à prendre en compte (encore une fois, vraiment importante si elle devient un problème) est que la taille de la stack est généralement fixe et peut être très inférieure à la taille du tas. Donc, si vous allouez de gros objects ou de nombreux petits objects, vous voudrez probablement utiliser le tas; Si vous manquez d’espace, le moteur d’exécution générera l’exception du site. Pas généralement une grosse affaire, mais une autre chose à considérer.

La stack est plus efficace et plus facile à gérer les données délimitées.

Mais heap devrait être utilisé pour tout ce qui est supérieur à quelques Ko (c’est facile en C ++, créez simplement un boost::scoped_ptr sur la stack pour boost::scoped_ptr un pointeur sur la mémoire allouée).

Considérons un algorithme récursif qui continue de s’appeler. Il est très difficile de limiter ou de deviner l’utilisation totale de la stack! Alors que sur le tas, l’allocateur ( malloc() ou new )) peut indiquer une mémoire NULL renvoyant NULL ou en lançant.

Source : Linux Kernel dont la stack ne dépasse pas 8 Ko!

Pour être complet, vous pouvez lire l’article de Miro Samek sur les problèmes d’utilisation du tas dans le contexte des logiciels intégrés .

Un tas de problèmes

Le choix de l’allocation sur le tas ou sur la stack est fait pour vous, en fonction de la manière dont votre variable est allouée. Si vous allouez quelque chose dynamicment, en utilisant un “nouvel” appel, vous allouez depuis le tas. Si vous atsortingbuez quelque chose en tant que variable globale ou en tant que paramètre dans une fonction, elle est affectée à la stack.

À mon avis, il y a deux facteurs décisifs

 1) Scope of variable 2) Performance. 

Je préférerais utiliser la stack dans la plupart des cas, mais si vous avez besoin d’accéder à une variable en dehors de la scope, vous pouvez utiliser heap.

Pour améliorer les performances lors de l’utilisation des segments de mémoire, vous pouvez également utiliser la fonctionnalité pour créer un bloc de mémoire et obtenir des performances au lieu d’atsortingbuer chaque variable à différents emplacements de mémoire.

cela a probablement été bien répondu. Je voudrais vous diriger vers la série d’articles ci-dessous pour mieux comprendre les détails de bas niveau. Alex Darby a une série d’articles dans lesquels il vous guide avec un débogueur. Voici la partie 3 sur la stack. http://www.altdevblogaday.com/2011/12/14/cc-low-level-curriculum-part-3-the-stack/