Pile, Statique, et le Tas en C++

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 affecter de variables dans le tas?

j'ai entendu que d'autres langues incorporent un" collecteur de déchets " pour que vous n'ayez pas à vous soucier de la mémoire. Quel est le garbage collector?

quoi pourriez-vous manipuler la mémoire par vous-même que vous ne pouviez pas faire en utilisant ce collecteur d'ordures?

une Fois quelqu'un m'a dit qu'avec cette déclaration:

int * asafe=new int;

j'ai un "pointeur vers un pointeur". Ça veut dire quoi? Il est différent de:

asafe=new int;

?

141
demandé sur user2864740 2009-01-03 08:41:58

9 réponses

une question similaire a été posée, mais il n'a pas demandé à propos de la statique.

Résumé de ce que statique, de tas et de la pile de la mémoire sont:

  • une variable statique est fondamentalement une variable globale, même si vous ne pouvez pas y accéder globalement. Habituellement, il y a une adresse pour cela qui est dans l'exécutable lui-même. Il n'y a qu'une copie pour l'ensemble du programme. Peu importe combien de fois vous allez dans un appel de fonction (ou classe) (et en combien de fils!) la variable se réfère au même emplacement de mémoire.

  • Le tas est un bouquet de mémoire qui peut être utilisé de façon dynamique. Si vous voulez 4kb pour un objet, alors l'allocateur dynamique va regarder à travers sa liste d'espace libre dans le tas, choisir un morceau de 4kb, et vous le donner. En général, l'allocateur de mémoire dynamique (malloc, new, etc.) ça commence à la fin de la mémoire et ça marche à l'envers.

  • expliquer comment une pile se développe et se rétrécit est un peu en dehors de la portée de cette réponse, mais il suffit de dire que vous ajoutez et supprimez toujours de la fin seulement. Les piles commencent habituellement haut et se développent vers le bas à 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 par opposition à la mémoire virtuelle et à la fragmentation). Plusieurs threads nécessiteront plusieurs stacks (le processus réserve généralement une taille minimale pour la pile).

quand vous voulez utiliser chacun:

  • Statique/globales sont utiles pour mémoire que vous savez que vous aurez toujours besoin et vous savez que vous ne voulez pas jamais désallouer. (Soit dit en passant, les environnements intégrés peuvent être considérés comme n'ayant qu'une mémoire statique... la pile et le tas sont partie d'un espace d'adressage partagé par un troisième type de mémoire: le code du programme. Les programmes font souvent une répartition dynamique hors de leur mémoire statique quand ils ont besoin de choses comme des listes lié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 plutôt d'autres objets sont alloués à partir de la mémoire détenue par le tampon à cette fin. Vous pouvez le faire également en mode non embarqué, et les jeux consoles éviteront souvent les mécanismes de mémoire dynamique intégrés en faveur d'un contrôle strict du processus d'allocation en utilisant des tampons de tailles prédéfinies pour toutes les allocations.)

  • Pile variables sont utiles quand vous savez que tant que la fonction est dans la portée (sur la pile quelque part), vous voulez les variables de rester. Les piles sont agréables pour les variables dont vous avez besoin pour le code où elles sont situées, mais qui n'est pas nécessaire en dehors de ce code. Ils sont également très agréables lorsque vous accédez à une ressource, comme un fichier, et que vous voulez que la ressource disparaisse automatiquement lorsque vous quittez ce code.

  • allocations de Tas (de mémoire allouée dynamiquement) est utile lorsque vous voulez être plus souple que le précédent. Souvent, une fonction est appelée pour répondre à un événement (l'utilisateur clique sur le bouton "Créer une boîte"). La réponse appropriée peut nécessiter l'allocation d'un nouvel objet (un nouvel objet Box) qui devrait rester longtemps après la fin de la fonction, de sorte qu'il ne peut pas être sur la pile. Mais vous ne savez pas combien de boîtes vous voulez au début du programme, il ne peut pas être un statique.

Collecte Des Ordures

j'ai beaucoup entendu dernièrement sur la façon dont les éboueurs sont grands, donc peut-être un peu d'une voix dissidente serait utile.

la collecte des ordures est un mécanisme merveilleux pour quand la performance n'est pas un problème énorme. J'ai entendu dire que la GCs s'améliore et devient plus sophistiquée, mais le fait est que vous pourriez être forcé d'accepter une pénalité de performance (selon le cas d'utilisation). Et si tu es paresseux, ça ne marchera peut-être pas. Dans le meilleur des cas, les éboueurs se rendent compte que votre mémoire disparaît quand ils se rendent compte qu'il n'y a plus de références à elle (voir comptage de référence ). Mais, si vous avez un objet qui se réfère à lui-même (éventuellement en se référant à un autre objet qui se réfère en arrière), alors le comptage de référence seul n'indiquera pas que la mémoire peut être supprimée. Dans ce cas, le GC doit examiner l'ensemble de la soupe et de la figure de référence. s'il y a des îles qui ne sont visées par eux-mêmes. De façon spontanée, je suppose que c'est une opération O(N^2), mais peu importe ce que c'est, ça peut devenir mauvais si vous êtes du tout concerné par la performance. (Edit: Martin B souligne qu'il est O(n) pour des algorithmes raisonnablement efficaces. C'est encore un peu trop si vous vous souciez de la performance et pouvez vous désallouer en temps constant sans collecte des ordures.)

Personnellement, quand j'entends des gens disons que C++ n'a pas de collecte des ordures, mon Esprit dit que c'est une caractéristique de C++, mais je suis probablement en minorité. Probablement la chose la plus difficile pour les gens à apprendre sur la programmation en C et C++ sont des pointeurs et comment gérer correctement leurs allocations de mémoire dynamique. Certaines autres langues, comme Python, seraient horribles sans GC, donc je pense que ça se résume à ce que vous voulez d'une langue. Si vous voulez des performances fiables, alors C++ sans collecte des ordures est la seule chose de ce côté de Fortran que je peux penser. Si vous voulez une facilité d'utilisation et des roues de formation (pour vous éviter de vous écraser sans avoir à apprendre la gestion de mémoire "appropriée"), 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 pouvez passer à optimiser d'autres codes. Il n'y a plus vraiment de pénalité de performance, mais si vous avez vraiment besoin d'une performance fiable (et la capacité de savoir exactement ce qui se passe, quand, sous les couvertures) alors je m'en tiendrais à C.++ Il y a une raison pour laquelle chaque moteur de jeu majeur que j'ai jamais entendu parler est en C++ (si ce n'est pas en C ou en assemblage). Python, et al sont très bien pour le script, mais pas le moteur de jeu principal.

204
répondu markets 2017-05-23 12:18:18

ce qui suit n'est évidemment pas tout à fait précis. Prendre avec un grain de sel quand vous le lire :)

Eh bien, les trois choses que vous vous référez sont durée de stockage automatique , statique et dynamique , qui a quelque chose à voir avec la durée de vie des objets et quand ils commencent la vie.


durée de stockage automatique

vous utilisez la durée de stockage automatique pour données de" courte durée et données de "petite , qui est nécessaire seulement localement dans un certain bloc:

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

La vie se termine dès que nous quittez le bloc, et ça commence dès que l'objet est défini. Ils sont le type le plus simple de durée de stockage, et sont beaucoup plus rapides que dans 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, qui peuvent être accédées par n'importe quel code à tout moment, si leur portée permet une telle utilisation (namespace scope), et pour les variables locales qui ont besoin d'étendre leur durée de vie à travers la sortie de leur portée (local scope), et pour les variables membres qui doivent être partagées par tous les objets de leur classe (classs scope). Leur vie dépend de leur champ de vision. Ils peuvent avoir namespace scope et portée locale et portée de la classe . Ce qui est vrai à leur sujet, 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 , parce que localA n'est pas détruit à la sortie de son bloc. Vous pouvez dire que les objets qui ont une portée locale commencent la durée de vie lorsque le contrôle atteint leur définition . Pour localA , cela se produit lorsque le corps de la fonction est entré. Pour les objets dans namespace scope, lifetime commence à 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, classScopeA n'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 indiquent le même objet. Il y a des règles spéciales sur quand et comment les objets statiques sont initialisé, mais ne nous en soucions pas maintenant. Qu'entend-on par le terme initialisation statique afin fiasco .


durée de stockage dynamique

La dernière durée de stockage est dynamique. Vous l'utilisez si vous voulez avoir des objets de vivre sur une autre île, et vous voulez mettre des pointeurs autour de cette référence. Vous les utilisez également si vos objets sont grand , et si vous voulez créer des tableaux de taille seulement connu à runtime . 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 la durée de vie lorsqu'une nouvelle l'invocation de l'opérateur 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 se termine seulement lorsque vous appelez supprimer pour eux. Si tu oublies ça, ces objets ne finissent jamais ta 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 manipulation manuelle de leur durée de vie et de la ressource mémoire associée. Les bibliothèques existent pour faciliter leur utilisation. Explicite la collecte des ordures pour objets ne peut être établie à 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 supprimer: le PTR partagé le fait pour vous, si le dernier pointeur qui renvoie l'objet sort de la portée. Le ptr partagé lui-même a une durée de stockage automatique. Ainsi sa durée de vie est gérée automatiquement, lui permettant de vérifier s'il doit supprimer l'objet pointé vers dynamique dans son destructeur. Pour la référence shared_ptr, voir les documents boost: http://www.boost.org/doc/libs/1_37_0/libs/smart_ptr/shared_ptr.htm

51
répondu Johannes Schaub - litb 2015-11-02 20:27:56

il a été dit en détail, 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 mondiale)

    durée de vie = programme d'exécution (1)

    visibilité = l'Unité de compilation est instanciée dans (2)

  • tas variable

    durée de vie = défini par vous (nouveau supprimer)

    visibilité = défini par vous (ce que vous affectez le pointeur)

  • "151950920 pile" variable

    visibilité = de la déclaration jusqu'à la sortie du champ d'application
    151980920" durée de vie = de la déclaration jusqu'à déclarer champ d'application est quittée


(1) plus exactement: de l'initialisation à la déinitialisation de l'Unité de compilation (c'est-à-dire le fichier c / c++). Ordre de l'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.

35
répondu peterchen 2011-02-10 07:14:32

je suis sûr que l'un des pédants va trouver une meilleure réponse sous peu, mais la principale différence est la vitesse et la taille.

Pile

Considérablement plus rapide à allouer. Il est fait en O(1) puisqu'il est alloué lors de la mise en place du cadre de la pile de sorte qu'il est essentiellement libre. L'inconvénient est que si vous manquez d'espace de pile vous sont désossées. Vous pouvez ajuster la taille de la pile, mais IIRC vous avez ~2MB pour jouer avec. Aussi, dès que vous quittez la fonction tout sur la pile est effacée. Il peut donc être problématique d'y faire référence plus tard. (Pointeurs pour empiler les objets alloués mène à des bogues.)

tas

drastiquement plus lent à allouer. Mais tu dois jouer avec GB, et pointer du doigt.

Collecteur D'Ordures

le collecteur d'ordures est un code qui court à l'arrière-plan et libère la mémoire. Lorsque vous allouer de la mémoire sur le tas, il est très facile d'oublier gratuit elle, qui est connu comme une fuite de mémoire. Avec le temps, la mémoire que votre application consomme croît et croît jusqu'à ce qu'elle s'effondre. Avoir un collecteur d'ordures périodiquement libérer la mémoire dont vous n'avez plus besoin aide à éliminer cette classe de bogues. Bien sûr, cela a un prix, car le ramasseur d'ordures ralentit les choses.

5
répondu Chris Smith 2009-01-03 06:06:29

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 nombre variable de données dont le nombre n'est pas connu avant 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 fait l'allocation retourne.

je pourrais écrire une application entière sans affecter de variables dans le tas?

peut-être, mais pas une application non-triviale, normale, grande (mais soi-disant" embedded " programmes pourraient être écrits sans le tas, en utilisant un sous-ensemble de c++).

Que fait le ramasseur d'ordures ?

il continue à regarder vos données ("mark and sweep") pour détecter quand votre application n'est pas plus de référencement. Ceci est pratique pour l'application, car l'application n'a pas besoin de libérer les données ... mais le collecteur d'ordures peut être coûteux sur le plan informatique.

les éboueurs ne sont pas une caractéristique habituelle de la programmation C++.

que pouvais-tu faire en manipulant la mémoire par toi-même que tu ne pouvais pas faire en utilisant ce collecteur d'ordures?

apprendre les mécanismes C++ pour déallocation déterministe de la mémoire:

  • "statique": ne jamais libéré
  • une "pile": dès que la variable "est hors de portée"
  • 'tas': lorsque le pointeur est supprimé (explicitement supprimé par la demande, ou implicitement supprimé dans un-ou-autre sous-programme)
3
répondu ChrisW 2009-01-03 06:12:21

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

vous pouvez faire couler la mémoire assez facilement sans un collecteur d'ordures, mais vous pouvez aussi dicter quand les objets et la mémoire sont libérés. J'ai eu des problèmes avec Java quand il exécute le GC et j'ai un processus en temps réel, parce que le GC est un thread exclusif (rien d'autre ne peut fonctionner). Donc, si la performance est critique et que vous pouvez garantir qu'il n'y a pas d'objets qui fuient, ne pas utiliser un GC est très utile. Sinon, il vous fait juste détester la vie quand votre application consomme de la mémoire et vous devez traquer la source d'une fuite.

1
répondu Rob Elsner 2009-01-03 06:09:58

Que faire si votre programme ne sait pas à l'avance combien de mémoire allouer (donc vous ne pouvez pas utiliser de variables de pile). Disons listes liées, les listes peuvent se développer sans savoir au préalable quelle est sa taille. Ainsi, allouer sur un tas a du sens pour une liste liée quand vous n'êtes pas conscient du nombre d'éléments qui y seraient insérés.

1
répondu kal 2009-01-03 06:36:24

un avantage du GC dans certaines situations est un ennui dans d'autres; compter sur le GC encourage à ne pas trop y penser. En théorie, attend jusqu'à la période "inactif" ou jusqu'à ce qu'il doit absolument, quand il va voler la bande passante et causer la latence de réponse dans votre application.

Mais tu n'as pas à ne pas y penser."Tout comme avec tout le reste dans les applications multithread, quand vous pouvez le rendement, vous pouvez rendement. Ainsi, par exemple, dans .Net, il est possible de demander un GC; par en faisant cela, au lieu de moins fréquents GC à plus long terme, vous pouvez avoir plus fréquents GC à plus court terme, et étaler la latence associée à ce overhead.

Mais cela va à l'encontre de la principale attraction de la GC qui semble être "encouragés à ne pas avoir à penser beaucoup à ce sujet parce que c'est de l'auto-mat-ic."

si vous avez d'abord été exposé à la programmation avant que le GC ne devienne prédominant et que vous étiez à l'aise avec malloc/free et new/delete, alors il pourrait même que vous trouviez GC un peu agaçant et/ou méfiant(comme on pourrait se méfier de "l'optimisation", qui a eu un passé vérifié.) De nombreuses applications tolèrent la latence aléatoire. Mais pour les applications qui ne le font pas, où la latence aléatoire est moins acceptable, une réaction courante est d'éviter les environnements GC et de se déplacer dans la direction d'un code purement non géré (ou Dieu nous en garde, un art de longue date mourant, le langage d'assemblage.)

j'ai eu un étudiant d'été ici il y a quelques temps, un interne, intelligent kid, qui s'appuyait sur GC; il était tellement admiratif à propos de la superposition de GC que même quand il programmait en C/C++ non géré, il refusait de suivre le modèle malloc/free new/delete parce que, je cite, "vous ne devriez pas avoir à faire cela dans un langage de programmation moderne."Et vous savez? Pour les applications minuscules et courtes, vous pouvez en effet vous en tirer avec cela, mais pas pour les applications performantes de longue durée.

1
répondu frediano 2013-05-22 13:06:52

Stack est une mémoire attribuée par le compilateur, lorsque nous compilons le programme, dans le compilateur par défaut attribue une certaine mémoire à partir de L'OS (nous pouvons changer les paramètres du compilateur dans votre IDE) et L'OS est celui qui vous donne la mémoire, son dépend de beaucoup de mémoire disponible sur le système et beaucoup d'autres choses, et venir à la mémoire de la pile est allouer lorsque nous déclarons une variable qu'ils copient (ref comme des formals) ces variables sont poussés sur la pile ils suivent quelques conventions de nommage par défaut de son CDECL dans Visual studios ex: notation infix: c=a+b; la poussée de pile se fait de droite à gauche en poussant, b à pile, opérateur, a à pile et résultat de ceux i,E C à pile. En pré fixer la notation: =+taxi Ici toutes les variables sont poussées à la pile 1 (de droite à gauche)et ensuite l'opération est faite. Ce mémoire allouée par le compilateur est fixe. Supposons donc que 1MB de mémoire est alloué à notre application, disons que les variables utilisées 700kb de mémoire(toutes les variables locales sont poussées à la pile sauf s'ils sont alloués dynamiquement) donc 324kb mémoire restante est alloué à tas. Et cette pile a moins de durée de vie, quand la portée de la fonction se termine ces piles sont effacées.

0
répondu raj 2013-11-06 04:50:32