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


122

Je programme depuis un moment mais c'est surtout Java et C #. Je n'ai jamais eu à gérer la mémoire par moi-même. J'ai récemment commencé à programmer en C ++ et je ne sais pas trop quand je dois stocker des choses sur la pile et quand les stocker sur le tas.

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


Réponses:


242

Non, la différence entre pile et 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 n'avez pas malloc () ou new) vit sur la pile. Il disparaît lorsque vous revenez de la fonction. Si vous voulez que quelque chose dure 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 une meilleure compréhension de ce qu'est la pile, allez-y de l'autre côté - plutôt que d'essayer de comprendre ce que fait la pile en termes de langage de haut niveau, recherchez "call stack" et "call convention" et voyez ce que la machine le fait vraiment lorsque vous appelez une fonction. La mémoire de l'ordinateur n'est qu'une série d'adresses; "tas" et "pile" sont des inventions du compilateur.


7
Il serait prudent d'ajouter que les informations de taille variable sont généralement stockées dans le tas. Les seules exceptions dont je suis conscient sont les VLA dans C99 (qui a un support limité) et la fonction alloca () qui est souvent mal comprise même par les programmeurs C.
Dan Olson

10
Bonne explication, bien que dans un scénario multithread avec des allocations et / ou des désallocations fréquentes, le tas est un point de discorde, affectant ainsi les performances. Pourtant, Scope est presque toujours le facteur décisif.
peterchen

18
Bien sûr, et new / malloc () est en soi une opération lente, et la pile est plus susceptible d'être dans dcache qu'une ligne de tas arbitraire. Ce sont des considérations réelles, mais généralement secondaires à la question de la durée de vie.
Crashworks

1
Est-ce vrai "La mémoire de l'ordinateur n'est qu'une série d'adresses;" tas "et" pile "sont des inventions de la compilation" ?? J'ai lu à de nombreux endroits que la pile est une région spéciale de la mémoire de notre ordinateur.
Vineeth Chitteti

2
@kai C'est une façon de le visualiser, mais ce n'est pas nécessairement vrai physiquement. Le système d'exploitation est responsable de l'allocation de la pile et du tas d'une application. Le compilateur est également responsable, mais il s'appuie principalement sur le système d'exploitation pour le faire. La pile est limitée et le tas ne l'est pas. Cela est dû à la façon dont le système d'exploitation gère le tri de ces adresses mémoire en quelque chose de plus structuré afin que plusieurs applications puissent s'exécuter sur le même système. Le tas et la pile ne sont pas les seuls, mais ce sont généralement les deux seuls qui préoccupent la plupart des développeurs.
tsturzl

42

Je dirais:

Stockez-le sur la pile, si vous POUVEZ.

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

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

  • C'est trop gros - sur les programmes multithread sur OS 32 bits, la pile a une taille petite et fixe (au moins au moment de la création du thread) (généralement quelques mégaoctets. Cela vous permet de créer beaucoup de threads sans épuiser l'adresse Pour les programmes 64 bits ou les programmes à un seul thread (Linux de toute façon), ce n'est pas un problème majeur. Sous Linux 32 bits, les programmes à un seul thread utilisent généralement des piles dynamiques qui peuvent continuer à croître jusqu'à ce qu'elles atteignent le sommet du tas.
  • Vous devez y accéder en dehors de la portée du cadre de pile d'origine - c'est vraiment la raison principale.

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


1
Tout ce qui dépasse quelques Ko est généralement préférable de mettre sur le tas. Je ne connais pas les détails mais je ne me souviens pas avoir jamais travaillé avec une pile qui faisait "quelques mégas".
Dan Olson

2
C'est quelque chose qui ne concernerait pas un utilisateur au début. Pour l'utilisateur, les vecteurs et les listes semblent être alloués sur la pile même si la STL stocke le contenu sur le tas. La question semblait plus sur la ligne de décider quand appeler explicitement nouveau / supprimer.
David Rodríguez - dribeas

1
Dan: J'ai mis 2 concerts (Oui, G comme dans GIGS) sur la pile sous Linux 32 bits. Les limites de pile dépendent du système d'exploitation.
Mr.Ree

6
mrree: La pile Nintendo DS fait 16 kilo-octets. Certaines limites de pile dépendent du matériel.
Ant

Ant: Toutes les piles dépendent du matériel, du système d'exploitation et également du compilateur.
Viliami

24

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 sur la pile et les données sur le tas en fonction de la façon dont vous les déclarez. Par exemple:

std::vector<int> v(10);

Dans le corps d'une fonction, cela déclare un vector(tableau dynamique) de dix entiers sur la pile. Mais le stockage géré par le vectorn'est pas sur la pile.

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 de vectorlui - même, qui ici est basée sur la pile, donc cela ne fait aucune différence comment il est implémenté - nous ne pouvons le traiter que comme un objet basé sur la pile avec une sémantique de valeur.

Non. Supposons que la fonction soit:

void GetSomeNumbers(std::vector<int> &result)
{
    std::vector<int> v(10);

    // fill v with numbers

    result.swap(v);
}

Ainsi, tout ce qui a une swapfonction (et tout type de valeur complexe devrait en avoir une) peut servir de sorte de référence rebindable à certaines données de tas, sous un système qui garantit un seul propriétaire de ces données.

Par conséquent, l'approche C ++ moderne consiste à ne jamais stocker l'adresse des données de tas dans des variables de pointeur local nues. Toutes les allocations de tas doivent être cachées à l'intérieur des 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 wrapper de type valeur pour certaines données de tas, ce qui devrait être inhabituel) .

Vous devez simplement conserver un peu de connaissances spéciales pour vous aider à optimiser: si possible, au lieu d'assigner une variable à une autre comme ceci:

a = b;

échangez-les comme ceci:

a.swap(b);

parce que c'est beaucoup plus rapide et ne lance pas d'exceptions. La seule exigence est que vous n'ayez pas besoin bde continuer à conserver la même valeur (elle obtiendra ala valeur de à la place, qui sera supprimée a = b).

L'inconvénient est que cette approche vous oblige à renvoyer les valeurs des fonctions via les paramètres de sortie au lieu de la valeur de retour réelle. Mais ils corrigent cela en C ++ 0x avec des références rvalue .

Dans les situations les plus compliquées de toutes, vous prendriez cette idée à l'extrême général et utiliseriez une classe de pointeur intelligent telle que celle shared_ptrqui est déjà dans tr1. (Bien que je soutienne que si vous semblez en avoir besoin, vous avez peut-être quitté le point idéal de l'applicabilité du C ++ standard.)


6

Vous stockeriez également un élément sur le tas s'il doit être utilisé en dehors de la portée de la fonction dans laquelle il est créé. Un idiome utilisé avec les objets de la pile est appelé RAII - cela implique l'utilisation de l'objet basé sur la pile comme enveloppe pour une ressource, lorsque l'objet est détruit, la ressource serait nettoyée. Les objets basés sur la pile sont plus faciles à suivre lorsque vous pouvez lever des exceptions - vous n'avez pas besoin de vous soucier de la suppression d'un objet basé sur le tas 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 peut être un wrapper basé sur une pile pour un pointeur brut vers un objet basé sur un tas.


5

Pour ajouter aux autres réponses, il peut aussi s'agir de performances, au moins un peu. Non pas que vous devriez vous en soucier à 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 à mesure que la mémoire se fragmente et / ou que vous vous approchez de l'utilisation de 100% de votre espace d'adressage. D'un autre côté, les allocations de pile sont des opérations à temps constant, essentiellement «gratuites».

Une autre chose à considérer (encore une fois, vraiment importante si cela devient un problème) est que généralement la taille de la pile est fixe et peut être beaucoup plus basse que la taille du tas. Donc, si vous allouez de gros objets ou de nombreux petits objets, vous souhaiterez probablement utiliser le tas; si vous manquez d'espace de pile, le runtime lèvera l'exception titulaire du site. Ce n'est généralement pas un gros problème, mais une autre chose à considérer.


Le tas et la pile sont de la mémoire virtuelle paginée. Le temps de recherche du tas est incroyablement rapide par rapport à ce qu'il faut pour mapper dans une nouvelle mémoire. Sous Linux 32 bits, je peux mettre> 2gig sur ma pile. Sous Mac, je pense que la pile est strictement limitée à 65Meg.
Mr.Ree

3

Stack est plus efficace et plus facile à gérer les données étendues.

Mais le tas doit être utilisé pour tout ce qui dépasse quelques Ko (c'est facile en C ++, il suffit de créer un boost::scoped_ptrsur la pile pour contenir un pointeur vers la mémoire allouée).

Considérez un algorithme récursif qui ne cesse de s'appeler. Il est très difficile de limiter et / ou de deviner l'utilisation totale de la pile! Alors que sur le tas, l'allocateur ( malloc()ou new) peut indiquer un manque de mémoire en retournant NULLou en throwing.

Source : noyau Linux dont la pile ne dépasse pas 8 Ko!


Pour référence aux autres lecteurs: (A) Le "devrait" ici est purement l'opinion personnelle de l'utilisateur, tirée au mieux d'une citation et d'un scénario que de nombreux utilisateurs sont peu susceptibles de rencontrer (récursivité). En outre, (B) la bibliothèque standard fournit std::unique_ptr, ce qui devrait être préféré à toute bibliothèque externe comme Boost (bien que cela alimente le standard au fil du temps).
underscore_d


1

Le choix d'allouer sur le tas ou sur la pile est fait pour vous, en fonction de la façon dont votre variable est allouée. Si vous allouez quelque chose de manière dynamique, en utilisant un "nouvel" appel, vous allouez à partir du tas. Si vous allouez quelque chose en tant que variable globale ou en tant que paramètre dans une fonction, il est alloué sur la pile.


4
Je soupçonne qu'il demandait quand mettre les choses sur le tas, pas comment.
Steve Rowe

0

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

1) Scope of variable
2) Performance.

Je préférerais utiliser stack dans la plupart des cas, mais si vous avez besoin d'accéder à une variable en dehors de la portée, vous pouvez utiliser le tas.

Pour améliorer les performances tout en utilisant des tas, vous pouvez également utiliser la fonctionnalité pour créer un bloc de tas et cela peut aider à gagner en performances plutôt que d'allouer chaque variable dans un emplacement mémoire différent.


0

cela a probablement été très bien répondu. Je voudrais vous signaler la série d'articles ci-dessous pour avoir une compréhension plus approfondie des détails de bas niveau. Alex Darby a une série d'articles dans lesquels il vous guide avec un débogueur. Voici la troisième partie sur la pile. http://www.altdevblogaday.com/2011/12/14/cc-low-level-curriculum-part-3-the-stack/


Le lien semble mort, mais la vérification de la machine Internet Archive Wayback indique qu'elle ne parle que de la pile et ne fait donc rien pour répondre à la question spécifique ici de pile par rapport à tas . -1
underscore_d
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.