web-dev-qa-db-fra.com

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

Je programme depuis un certain temps mais c'est surtout Java et C #. Je n'ai jamais vraiment eu à gérer la mémoire par moi-même. J'ai récemment commencé à programmer en C++ et je suis un peu confus quant au moment où je devrais stocker des choses sur la pile et quand les stocker sur le tas.

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

121
Alexander

Non, la différence entre la pile 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 n'avez pas malloc () ou nouveau) vit sur la pile. 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 une compréhension plus claire de ce qu'est la pile, venez 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 "pile d'appel" et "convention d'appel" et voyez ce que la machine fait vraiment quand 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.

240
Crashworks

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 grand - sur les programmes multithread sur un système d'exploitation 32 bits, la pile a une taille petite et fixe (au moment de la création des threads au moins) (généralement seulement quelques mégaoctets. Cela vous permet de créer de nombreux threads sans épuiser l'adresse l'espace. 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 de croître jusqu'à ce qu'elles atteignent le haut du tas.
  • Vous devez y accéder en dehors du cadre du cadre de pile d'origine - c'est vraiment la principale raison.

Il est possible, avec des compilateurs raisonnables, 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).

42
MarkR

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

Mais non. Supposons que la fonction était:

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

    // fill v with numbers

    result.swap(v);
}

Donc, tout ce qui a une fonction swap (et tout type de valeur complexe devrait en avoir une) peut servir de sorte de référence réassociable à 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 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 d'encapsuleur de type valeur pour certaines données de tas, ce qui devrait être inhabituel) .

Vous n'avez qu'à conserver un peu de connaissances spéciales pour vous aider à optimiser: si possible, au lieu d'affecter une variable à une autre comme ceci:

a = b;

les échanger comme ceci:

a.swap(b);

car il est beaucoup plus rapide et ne lève pas d'exceptions. La seule condition est que vous n'avez pas besoin de b pour continuer à conserver la même valeur (cela va obtenir la valeur de a à la place, qui serait mise à la corbeille dans 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 références rvalue .

Dans les situations les plus compliquées de tous, vous devriez pousser cette idée à l'extrême et utiliser une classe de pointeur intelligent telle que shared_ptr qui est déjà en tr1. (Bien que je soutienne que si vous semblez en avoir besoin, vous avez peut-être dépassé la zone d'applicabilité de Standard C++.)

24
Daniel Earwicker

Vous devez également stocker 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 pile est appelé RAII - cela implique d'utiliser l'objet basé sur la pile comme enveloppe pour une ressource, lorsque l'objet est détruit, la ressource est nettoyée. Les objets basés sur la pile sont plus faciles à suivre quand vous pouvez lever des exceptions - vous n'avez pas à vous soucier de supprimer un objet basé sur un 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 la pile pour un pointeur brut vers un objet basé sur le tas.

6
1800 INFORMATION

Pour ajouter aux autres réponses, il peut également s'agir de performances, au moins un peu. Non pas que vous deviez vous en préoccuper à 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 frais généraux). Cela peut devenir plus lent à mesure que la mémoire se fragmente et/ou que vous vous approchez d'utiliser 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, ce n'est vraiment important que si cela devient un problème) est que généralement la taille de la pile est fixe et peut être bien inférieure à la taille du tas. Donc, si vous allouez de gros objets ou de nombreux petits objets, vous voudrez probablement utiliser le tas; si vous manquez d'espace de pile, le runtime lèvera l'exception de titre du site. Ce n'est généralement pas un gros problème, mais une autre chose à considérer.

5
Nick

La pile est plus efficace et plus facile à gérer les données de portée.

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_ptr sur la pile pour contenir un pointeur sur la mémoire allouée).

Considérez un algorithme récursif qui continue d'appeler en lui-même. Il est très difficile de limiter ou de deviner l'utilisation totale de la pile! Alors que sur le tas, l'allocateur (malloc() ou new) peut indiquer une mémoire insuffisante en renvoyant NULL ou throw ing.

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

3
unixman83

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

n tas de problèmes

2
Daniel Daranas

Le choix d'allouer sur le tas ou sur la pile est fait pour vous, selon la façon dont votre variable est allouée. Si vous allouez quelque chose dynamiquement, 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.

1
Rob Lachlan

cela a probablement été assez 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, où il vous guide à travers un débogueur. Voici la partie 3 sur la pile. http://www.altdevblogaday.com/2011/12/14/c-c-low-level-curriculum-part-3-the-stack/

0
hAcKnRoCk

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

1) Scope of variable
2) Performance.

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

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

0
anand