Pile, statique et tas en C ++

160

J'ai cherché, mais je n'ai pas très bien compris ces trois concepts. Quand dois-je utiliser l'allocation dynamique (dans le tas) et quel est son réel avantage? Quels sont les problèmes de statique et de pile? Puis-je écrire une application entière sans allouer de variables dans le tas?

J'ai entendu dire que d'autres langages incorporaient un "garbage collector" pour que vous n'ayez pas à vous soucier de la mémoire. Que fait le garbage collector?

Que pourriez-vous faire en manipulant la mémoire par vous-même que vous ne pourriez pas faire en utilisant ce garbage collector?

Une fois, quelqu'un m'a dit cela avec cette déclaration:

int * asafe=new int;

J'ai un "pointeur vers un pointeur". Qu'est-ce que ça veut dire? C'est différent de:

asafe=new int;

?

Hai
la source
Une question très similaire a été posée il y a quelque temps: quoi et où se trouvent la pile et le tas? Il y a quelques très bonnes réponses à cette question qui devraient éclairer la vôtre.
Scott Saad
Copie possible de Quoi et où se trouvent la pile et le tas?
Swati Garg

Réponses:

223

Une question similaire a été posée, mais elle ne portait pas sur la statique.

Récapitulatif de la mémoire statique, du tas et de la pile:

  • Une variable statique est essentiellement une variable globale, même si vous ne pouvez pas y accéder globalement. Habituellement, il y a une adresse pour celui-ci qui se trouve dans l'exécutable lui-même. Il n'y a qu'une seule copie pour l'ensemble du programme. Peu importe combien de fois vous entrez dans un appel de fonction (ou classe) (et dans combien de threads!), La variable fait référence au même emplacement mémoire.

  • Le tas est un tas de mémoire qui peut être utilisé dynamiquement. Si vous voulez 4 Ko pour un objet, l'allocateur dynamique examinera sa liste d'espace libre dans le tas, choisira un morceau de 4 Ko et vous le donnera. Généralement, l'allocateur de mémoire dynamique (malloc, new, etc.) démarre à la fin de la mémoire et fonctionne à l'envers.

  • Expliquer comment une pile se développe et se réduit est un peu en dehors de la portée de cette réponse, mais il suffit de dire que vous ajoutez et supprimez toujours à la fin uniquement. Les piles commencent généralement haut et se développent vers des adresses inférieures. Vous manquez de mémoire lorsque la pile rencontre l'allocateur dynamique quelque part au milieu (mais faites référence à la mémoire physique et virtuelle et à la fragmentation). Plusieurs threads nécessiteront plusieurs piles (le processus réserve généralement une taille minimale pour la pile).

Quand vous voudriez utiliser chacun d'eux:

  • Les statiques / globales sont utiles pour la mémoire dont vous savez que vous aurez toujours besoin et vous savez que vous ne voulez jamais désallouer. (À propos, les environnements embarqués peuvent être considérés comme n'ayant que de la mémoire statique ... la pile et le tas font partie d'un espace d'adressage connu partagé par un troisième type de mémoire: le code du programme. Les programmes feront souvent une allocation dynamique à partir de leur de la mémoire statique lorsqu'ils ont besoin d'éléments tels que des listes chaînées. Mais quoi qu'il en soit, la mémoire statique elle-même (le tampon) n'est pas elle-même «allouée», mais d'autres objets sont alloués en dehors de la mémoire détenue par le tampon à cette fin. Vous pouvez le faire dans les non-embarqués également, et les jeux sur console évitent fréquemment les mécanismes de mémoire dynamique intégrés au profit d'un contrôle strict du processus d'allocation en utilisant des tampons de tailles prédéfinies pour toutes les allocations.)

  • Les variables de pile sont utiles lorsque vous savez que tant que la fonction est dans la portée (sur la pile quelque part), vous voudrez que les variables restent. Les piles sont bien pour les variables dont vous avez besoin pour le code où elles se trouvent, mais qui ne sont pas nécessaires en dehors de ce code. Ils sont également très utiles lorsque vous accédez à une ressource, comme un fichier, et que vous souhaitez que la ressource disparaisse automatiquement lorsque vous quittez ce code.

  • Les allocations de tas (mémoire allouée dynamiquement) sont utiles lorsque vous souhaitez être plus flexible que ce qui précède. Fréquemment, une fonction est appelée pour répondre à un événement (l'utilisateur clique sur le bouton "créer une boîte"). La bonne réponse peut nécessiter l'allocation d'un nouvel objet (un nouvel objet Box) qui devrait rester longtemps après la sortie de la fonction, donc il ne peut pas être sur la pile. Mais vous ne savez pas combien de boîtes vous voudriez au début du programme, donc ça ne peut pas être statique.

Collecte des ordures

J'ai beaucoup entendu ces derniers temps sur la qualité des Garbage Collectors, alors peut-être qu'un peu de voix dissidente serait utile.

Le nettoyage de la mémoire est un mécanisme formidable lorsque les performances ne sont pas un problème majeur. J'entends que les GC s'améliorent et deviennent de plus en plus sophistiqués, mais le fait est que vous pouvez être obligé d'accepter une pénalité de performance (selon le cas d'utilisation). Et si vous êtes paresseux, cela peut ne pas fonctionner correctement. Dans le meilleur des cas, les Garbage Collectors réalisent que votre mémoire disparaît quand il se rend compte qu'il n'y a plus de références à elle (voir le comptage des références). Mais, si vous avez un objet qui se réfère à lui-même (éventuellement en faisant référence à un autre objet qui renvoie), le comptage de références seul n'indiquera pas que la mémoire peut être supprimée. Dans ce cas, le GC doit examiner toute la soupe de référence et déterminer s'il existe des îles auxquelles elles ne font référence que par elles-mêmes. De façon désinvolte, je suppose que c'est une opération O (n ^ 2), mais quoi que ce soit, cela peut devenir mauvais si vous êtes préoccupé par les performances. (Edit: Martin B souligne que c'est O (n) pour les algorithmes raisonnablement efficaces. C'est encore O (n) trop si vous êtes préoccupé par les performances et pouvez désallouer en temps constant sans garbage collection.)

Personnellement, quand j'entends les gens dire que C ++ n'a pas de ramasse-miettes, mon esprit marque cela comme une fonctionnalité de C ++, mais je suis probablement minoritaire. Les pointeurs et la manière de gérer correctement leurs allocations de mémoire dynamique sont probablement la chose la plus difficile pour les gens à apprendre sur la programmation en C et C ++. Certains autres langages, comme Python, seraient horribles sans GC, donc je pense que cela revient à ce que vous voulez d'un langage. Si vous voulez des performances fiables, alors C ++ sans garbage collection est la seule chose à laquelle je puisse penser de ce côté de Fortran. Si vous voulez la facilité d'utilisation et la formation des roues (pour vous éviter de planter sans avoir besoin d'apprendre la gestion de la mémoire "correcte"), choisissez quelque chose avec un GC. Même si vous savez bien gérer la mémoire, cela vous fera gagner du temps que vous pourrez consacrer à l'optimisation d'autres codes. Il n'y a plus vraiment de pénalité en termes de performances, mais si vous avez vraiment besoin de performances fiables (et de la capacité de savoir exactement ce qui se passe, quand, sous les couvertures), je m'en tiendrai au C ++. Il y a une raison pour laquelle tous les principaux moteurs de jeu dont j'ai entendu parler sont en C ++ (sinon en C ou en assemblage). Python, et autres conviennent bien pour les scripts, mais pas pour le moteur de jeu principal.

marchés
la source
Ce n'est pas vraiment pertinent par rapport à la question d'origine (ou trop du tout, en fait), mais vous avez les emplacements de la pile et du tas à l'envers. En règle générale , la pile grossit et le tas grandit (bien qu'un tas ne "grandisse" pas réellement, donc c'est une simplification excessive) ...
P Daddy
Je ne pense pas que cette question soit similaire ou même dupliquée de l'autre question. celui-ci concerne spécifiquement C ++ et ce qu'il voulait dire est presque certainement les trois durées de stockage existant en C ++. Vous pouvez avoir un objet dynamique alloué sur la mémoire statique très bien, par exemple, overload op new.
Johannes Schaub - litb
7
Votre traitement désobligeant de la collecte des ordures a été un peu moins qu'utile.
P Daddy
9
Souvent, le garbage collection est de nos jours préférable à la libération manuelle de mémoire, car elle se produit lorsqu'il y a peu de travail à faire, par opposition à la libération de mémoire qui peut se produire lorsque les performances pourraient être utilisées autrement.
Georg Schölly
3
Juste un petit commentaire - le garbage collection n'a pas de complexité O (n ^ 2) (ce serait en effet désastreux pour les performances). Le temps nécessaire pour un cycle de garbage collection est proportionnel à la taille du tas - voir hpl.hp.com/personal/Hans_Boehm/gc/complexity.html .
Martin B
54

Ce qui suit n'est bien sûr pas tout à fait précis. Prenez-le avec un grain de sel quand vous le lisez :)

Eh bien, les trois choses auxquelles vous faites référence sont la durée de stockage automatique, statique et dynamique , qui a quelque chose à voir avec la durée de vie des objets et le moment où ils commencent leur vie.


Durée de stockage automatique

Vous utilisez la durée de stockage automatique pour les données de courte durée et de petite taille , qui n'est nécessaire que localement dans un bloc:

if(some condition) {
    int a[3]; // array a has automatic storage duration
    fill_it(a);
    print_it(a);
}

La durée de vie se termine dès que nous sortons du bloc, et elle démarre dès que l'objet est défini. Il s'agit du type de durée de stockage le plus simple et beaucoup plus rapide que la durée de stockage dynamique en particulier.


Durée de stockage statique

Vous utilisez la durée de stockage statique pour les variables libres, auxquelles n'importe quel code peut accéder à tout moment, si leur portée permet une telle utilisation (étendue de l'espace de noms), et pour les variables locales qui doivent étendre leur durée de vie à la sortie de leur étendue (portée locale), et pour les variables membres qui doivent être partagées par tous les objets de leur classe (portée des classes). Leur durée de vie dépend de la portée qu'ils sont. Ils peuvent avoir la portée d'espace de noms et la portée locale et la portée de la classe . Ce qui est vrai pour les deux, c'est qu'une fois leur vie commencée, leur vie se termine à la fin du programme . Voici deux exemples:

// static storage duration. in global namespace scope
string globalA; 
int main() {
    foo();
    foo();
}

void foo() {
    // static storage duration. in local scope
    static string localA;
    localA += "ab"
    cout << localA;
}

Le programme imprime ababab, car il localAn'est pas détruit à la sortie de son bloc. Vous pouvez dire que les objets qui ont une portée locale commencent leur vie lorsque le contrôle atteint leur définition . Car localA, cela se produit lorsque le corps de la fonction est entré. Pour les objets dans la portée de l'espace de noms, la durée de vie commence au démarrage du programme . Il en va de même pour les objets statiques de portée de classe:

class A {
    static string classScopeA;
};

string A::classScopeA;

A a, b; &a.classScopeA == &b.classScopeA == &A::classScopeA;

Comme vous le voyez, classScopeAn'est pas lié à des objets particuliers de sa classe, mais à la classe elle-même. L'adresse des trois noms ci-dessus est la même et tous désignent le même objet. Il existe une règle spéciale sur le moment et la manière dont les objets statiques sont initialisés, mais ne nous en préoccupons pas maintenant. Cela signifie le terme fiasco d'ordre d'initialisation statique .


Durée de stockage dynamique

La dernière durée de stockage est dynamique. Vous l'utilisez si vous souhaitez que des objets vivent sur une autre île et que vous souhaitez placer des pointeurs autour de cette référence. Vous les utilisez également si vos objets sont volumineux et si vous souhaitez créer des tableaux de taille connue uniquement à l' exécution . En raison de cette flexibilité, les objets ayant une durée de stockage dynamique sont compliqués et lents à gérer. Les objets ayant cette durée dynamique commencent leur durée de vie lorsqu'un nouvel appel d'opérateur approprié se produit:

int main() {
    // the object that s points to has dynamic storage 
    // duration
    string *s = new string;
    // pass a pointer pointing to the object around. 
    // the object itself isn't touched
    foo(s);
    delete s;
}

void foo(string *s) {
    cout << s->size();
}

Sa durée de vie ne prend fin que lorsque vous appelez delete pour eux. Si vous oubliez cela, ces objets ne terminent jamais leur vie. Et les objets de classe qui définissent un constructeur déclaré par l'utilisateur ne verront pas leurs destructeurs appelés. Les objets ayant une durée de stockage dynamique nécessitent une gestion manuelle de leur durée de vie et de la ressource mémoire associée. Des bibliothèques existent pour faciliter leur utilisation. Un garbage collection explicite pour des objets particuliers peut être établi à l'aide d'un pointeur intelligent:

int main() {
    shared_ptr<string> s(new string);
    foo(s);
}

void foo(shared_ptr<string> s) {
    cout << s->size();
}

Vous n'avez pas à vous soucier d'appeler delete: le ptr partagé le fait pour vous, si le dernier pointeur qui fait référence à l'objet sort de la portée. Le ptr partagé lui-même a une durée de stockage automatique. Donc , sa durée de vie est gérée automatiquement, ce qui permet de vérifier si elle doit supprimer l'objet pointu dynamique dans son destructor. Pour plus d'informations sur shared_ptr, consultez les documents boost: http://www.boost.org/doc/libs/1_37_0/libs/smart_ptr/shared_ptr.htm

Johannes Schaub - litb
la source
39

Il a été dit de manière élaborée, tout comme "la réponse courte":

  • variable statique (classe)
    durée de vie = exécution du programme (1)
    visibilité = déterminée par les modificateurs d'accès (privé / protégé / public)

  • variable statique (portée globale)
    durée de vie = exécution du programme (1)
    visibilité = l'unité de compilation dans laquelle elle est instanciée (2)


  • durée de vie de la variable de tas = définie par vous (nouveau à supprimer)
    visibilité = définie par vous (quoi que vous affectiez le pointeur)

  • pile variable
    visibilité = de la déclaration jusqu'à la sortie de la portée
    durée de vie = de la déclaration jusqu'à la sortie de la déclaration de la portée


(1) plus exactement: de l'initialisation jusqu'à la désinitialisation de l'unité de compilation (ie fichier C / C ++). L'ordre d'initialisation des unités de compilation n'est pas défini par la norme.

(2) Attention: si vous instanciez une variable statique dans un en-tête, chaque unité de compilation obtient sa propre copie.

Peterchen
la source
5

Je suis sûr que l'un des pédants trouvera bientôt une meilleure réponse, mais la principale différence est la vitesse et la taille.

Empiler

Affectation considérablement plus rapide. Cela se fait en O (1) car il est alloué lors de la mise en place du frame de pile, donc il est essentiellement libre. L'inconvénient est que si vous manquez d'espace dans la pile, vous êtes désossé. Vous pouvez ajuster la taille de la pile, mais IIRC vous avez ~ 2 Mo pour jouer. De plus, dès que vous quittez la fonction, tout ce qui se trouve sur la pile est effacé. Il peut donc être problématique de s'y référer plus tard. (Les pointeurs pour empiler les objets alloués entraînent des bogues.)

Tas

L'allocation est considérablement plus lente. Mais vous avez GB pour jouer avec et pointer.

Éboueur

Le garbage collector est un code qui s'exécute en arrière-plan et libère de la mémoire. Lorsque vous allouez de la mémoire sur le tas, il est très facile d'oublier de le libérer, ce que l'on appelle une fuite de mémoire. Au fil du temps, la mémoire consommée par votre application augmente et augmente jusqu'à ce qu'elle se bloque. Avoir un ramasse-miettes qui libère périodiquement la mémoire dont vous n'avez plus besoin aide à éliminer cette classe de bogues. Bien sûr, cela a un prix, car le ramasse-miettes ralentit les choses.

Chris Smith
la source
3

Quels sont les problèmes de statique et de pile?

Le problème avec l'allocation "statique" est que l'allocation est faite au moment de la compilation: vous ne pouvez pas l'utiliser pour allouer un certain nombre variable de données, dont le nombre n'est connu qu'au moment de l'exécution.

Le problème avec l'allocation sur la "pile" est que l'allocation est détruite dès que le sous-programme qui effectue l'allocation revient.

Je pourrais écrire une application entière sans allouer de variables dans le tas?

Peut-être mais pas une grosse application normale, non triviale (mais les programmes dits «embarqués» pourraient être écrits sans le tas, en utilisant un sous-ensemble de C ++).

Que fait le ramasse-miettes?

Il surveille constamment vos données («marquer et balayer») pour détecter quand votre application ne les référence plus. C'est pratique pour l'application, car l'application n'a pas besoin de désallouer les données ... mais le garbage collector peut être coûteux en calcul.

Les garbage collector ne sont pas une fonctionnalité habituelle de la programmation C ++.

Que pourriez-vous faire en manipulant la mémoire par vous-même que vous ne pourriez pas faire en utilisant ce garbage collector?

Apprenez les mécanismes C ++ pour la désallocation de mémoire déterministe:

  • 'static': jamais désalloué
  • 'stack': dès que la variable "sort du champ"
  • 'tas': lorsque le pointeur est supprimé (explicitement supprimé par l'application, ou implicitement supprimé dans un sous-programme ou un autre)
ChrisW
la source
1

L'allocation de mémoire de pile (variables de fonction, variables locales) peut être problématique lorsque votre pile est trop "profonde" et que vous débordez de la mémoire disponible pour les allocations de pile. Le tas est destiné aux objets qui doivent être accessibles à partir de plusieurs threads ou tout au long du cycle de vie du programme. Vous pouvez écrire un programme entier sans utiliser le tas.

Vous pouvez facilement perdre de la mémoire sans garbage collector, mais vous pouvez également dicter le moment où les objets et la mémoire sont libérés. J'ai rencontré des problèmes avec Java lorsqu'il exécute le GC et j'ai un processus en temps réel, car le GC est un thread exclusif (rien d'autre ne peut fonctionner). Donc, si les performances sont critiques et que vous pouvez garantir qu'il n'y a pas de fuite d'objets, ne pas utiliser de GC est très utile. Sinon, cela vous fait juste détester la vie lorsque votre application consomme de la mémoire et que vous devez rechercher la source d'une fuite.

Rob Elsner
la source
1

Que faire si votre programme ne sait pas à l'avance la quantité de mémoire à allouer (par conséquent, vous ne pouvez pas utiliser de variables de pile). Disons les listes liées, les listes peuvent s'agrandir sans savoir à l'avance quelle est sa taille. L'allocation sur un tas est donc logique pour une liste chaînée lorsque vous ne savez pas combien d'éléments y seraient insérés.

Kal
la source
0

Un avantage de GC dans certaines situations est une gêne dans d'autres; le fait de se fier à GC encourage à ne pas trop y penser. En théorie, attend la période `` d'inactivité '' ou jusqu'à ce qu'elle le doive absolument, lorsqu'elle volera de la bande passante et provoquera une latence de réponse dans votre application.

Mais vous n'avez pas à «ne pas y penser». Comme pour tout le reste des applications multithreads, lorsque vous pouvez céder, vous pouvez céder. Ainsi par exemple, dans .Net, il est possible de demander un GC; en faisant cela, au lieu d'un GC plus court et moins fréquent, vous pouvez avoir un GC plus court et plus fréquent et répartir la latence associée à cette surcharge.

Mais cela va à l'encontre de l'attrait principal de GC qui semble être «encouragé à ne pas trop y penser car il est automatique».

Si vous avez d'abord été exposé à la programmation avant que GC ne devienne répandu et que vous étiez à l'aise avec malloc / free et new / delete, il se peut même que vous trouviez GC un peu ennuyeux et / ou méfiant (car on pourrait se méfier de ' optimisation », qui a eu une histoire mouvementée.) De nombreuses applications tolèrent une latence aléatoire. Mais pour les applications qui ne le font pas, où la latence aléatoire est moins acceptable, une réaction courante consiste à éviter les environnements GC et à s'orienter vers un code purement non géré (ou à Dieu ne plaise, un art qui meurt depuis longtemps, le langage d'assemblage).

J'avais un étudiant d'été ici il y a quelque temps, un stagiaire, un enfant intelligent, qui a été sevré sur GC; il adorait tellement la supériorité de GC que même en programmant en C / C ++ non managé, il refusait de suivre le modèle malloc / free new / delete parce que, citons, "vous ne devriez pas avoir à faire cela dans un langage de programmation moderne." Et vous savez? Pour les applications minuscules et à exécution courte, vous pouvez en effet vous en tirer, mais pas pour les applications performantes de longue durée.

Frediano
la source
0

Stack est une mémoire allouée par le compilateur, chaque fois que nous compilons le programme, par défaut, le compilateur alloue de la mémoire à partir du système d'exploitation (nous pouvons modifier les paramètres des paramètres du compilateur dans votre IDE) et le système d'exploitation est celui qui vous donne la mémoire, cela dépend sur beaucoup de mémoire disponible sur le système et bien d'autres choses, et venir à la mémoire de la pile est allouée lorsque nous déclarons une variable qu'ils copient (réf comme formelle) ces variables sont poussées à empiler elles suivent certaines conventions de nommage par défaut son CDECL dans Visual studios ex: notation infixe: c = a + b; le poussage de la pile se fait de droite à gauche PUSHING, b pour empiler, opérateur, a pour empiler et résultat de ces i, ec pour empiler. En notation pré-fixe: = + cab Ici, toutes les variables sont poussées dans la première pile (de droite à gauche), puis l'opération est effectuée. Cette mémoire allouée par le compilateur est fixe. Supposons donc que 1 Mo de mémoire soit alloué à notre application, disons que les variables utilisent 700 Ko de mémoire (toutes les variables locales sont poussées vers la pile à moins qu'elles ne soient allouées dynamiquement) de sorte que la mémoire restante de 324 Ko est allouée au tas. Et cette pile a moins de durée de vie, lorsque la portée de la fonction se termine, ces piles sont effacées.

raj
la source