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? Pourrais-je écrire une application entière sans allouer de variables dans le tas?
J'ai entendu dire que d'autres langues intègrent un "ramasse-miettes" afin que vous n'ayez pas à vous soucier de la mémoire. Que fait le ramasse-miettes?
Que pourriez-vous faire en manipulant vous-même la mémoire que vous ne pourriez pas utiliser avec ce ramasse-miettes?
Une fois, quelqu'un m'a dit qu'avec cette déclaration:
int * asafe=new int;
J'ai un "pointeur sur un pointeur". Qu'est-ce que ça veut dire? C'est différent de:
asafe=new int;
?
ne question similaire a été posée, mais elle n’a pas posé de question sur la statique.
Une variable statique est fondamentalement une variable globale, même si vous ne pouvez pas y accéder globalement. Habituellement, son adresse se trouve dans l'exécutable lui-même. Il n'y a qu'un seul exemplaire pour l'ensemble du programme. Peu importe le nombre de fois que 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 examine la liste des espaces libres dans le segment de mémoire, choisit un bloc de 4 Ko et vous le donne. En règle générale, l'allocateur de mémoire dynamique (malloc, new, etc.) commence en fin de mémoire et fonctionne à l'envers.
Expliquer comment une pile grandit et se rétrécit dépasse un peu le cadre de cette réponse, mais il suffit de dire que vous ajoutez toujours et supprimez uniquement à la fin. Les piles commencent généralement haut et grandissent à des adresses plus basses. Vous manquez de mémoire lorsque la pile rencontre l'allocateur dynamique quelque part au milieu (mais vous vous référez à 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 à la pile).
Les statiques/globales sont utiles pour la mémoire dont vous avez toujours besoin et que vous ne voulez jamais désallouer. (En passant, 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 adresse connu partagé par un troisième type de mémoire: le code de programme. Les programmes font souvent de l’allocation dynamique en dehors de leur mémoire statique quand ils ont besoin d'éléments comme 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 à cet effet. Dans les jeux non intégrés également, les jeux sur console évitent souvent 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 (quelque part sur la pile), vous souhaiterez que les variables restent. Les piles sont pratiques 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 souhaitez que la ressource disparaisse automatiquement lorsque vous laissez 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 réponse appropriée peut nécessiter l'allocation d'un nouvel objet (un nouvel objet Box) qui devrait rester longtemps après la sortie de la fonction, de sorte qu'il ne puisse pas être sur la pile. Mais vous ne savez pas combien de boîtes vous voudriez au début du programme, donc cela ne peut pas être statique.
Ces derniers temps, j'ai beaucoup entendu parler de la grande qualité des éboueurs. Une voix dissidente serait peut-être utile.
Le ramassage des ordures est un mécanisme formidable lorsque la performance n’est pas un problème énorme. J'entends dire que les GC deviennent de plus en plus sophistiqués et sophistiqués, mais le fait est que vous pouvez être forcé d'accepter une pénalité de performance (selon le cas d'utilisation). Et si vous êtes paresseux, il se peut que cela ne fonctionne toujours pas correctement. Dans le meilleur des cas, les collecteurs de déchets réalisent que votre mémoire disparaît quand il se rend compte qu'il n'y a plus de références à celle-ci (voir comptage de références ). Toutefois, si vous avez un objet qui se réfère à lui-même (éventuellement en faisant référence à un autre objet qui renvoie en arrière), 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 l'ensemble de la soupe de référence et déterminer s'il existe des îles désignées par elles-mêmes. De prime abord, j'imagine que c'est une opération O (n ^ 2), mais quoi que ce soit, cela peut dégénérer si vous êtes du tout concerné par la performance. (Edit: Martin B fait remarquer que c'est O(n) pour des algorithmes raisonnablement efficaces. C'est toujours O(n) = trop si vous êtes soucieux de performance et pouvez désallouer en temps constant sans garbage collection.)
Personnellement, quand j'entends des gens dire que le C++ n’a pas de collecte des ordures, mon esprit l’a étiqueté comme une fonctionnalité du C++, mais je suis probablement minoritaire. La plupart des choses difficiles à apprendre sur la programmation en C et C++ sont probablement les pointeurs et comment gérer correctement leurs allocations de mémoire dynamiques. Certaines autres langues, comme Python, seraient horribles sans GC, alors je pense que cela dépend de ce que vous voulez dans une langue. Si vous voulez des performances fiables, C++ sans récupération de place est la seule chose à laquelle je peux penser de ce côté de Fortran. Si vous voulez une facilité d'utilisation et des roues d'entraînement (pour vous éviter toute collision sans que vous ayez à apprendre la gestion "appropriée" de la mémoire), 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'un autre code. Il n’ya plus vraiment de pénalité en termes de performances, mais si vous avez vraiment besoin de performances fiables (et de savoir exactement ce qui se passe, quand, à l’abri des couvertures), je me contenterais du C++. Il y a une raison pour laquelle chaque moteur de jeu majeur dont j'ai jamais entendu parler est en C++ (si ce n'est en C ou Assembly). Python, et autres conviennent pour les scripts, mais pas pour le moteur de jeu principal.
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 leur début dans la vie. .
Vous utilisez la durée de stockage automatique pour des données de courte durée et de petite taille , c'est-à-dire nécessaire seulement 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 prend fin dès que nous sortons du bloc et commence dès que l'objet est défini. Ils représentent le type de durée de stockage le plus simple et sont bien plus rapides que la durée de stockage dynamique en particulier.
Vous utilisez la durée de stockage statique pour les variables libres, auxquelles tout code peut accéder à tout moment, si leur portée autorise une telle utilisation (portée de l'espace de noms), et pour les variables locales qui ont besoin d'étendre leur durée de vie à la sortie de leur portée (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 dans laquelle ils se trouvent. Ils peuvent avoir une portée d'espace de noms et une portée locale et portée de la classe . Ce qui est vrai à propos d’eux deux, c’est qu’une fois leur vie commencée, celle-ci 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 ayant une portée locale commencent leur 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 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, 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 dénote 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 inquiétons pas de cela maintenant. Cela signifie le terme fiasco de l'ordre d'initialisation statique.
La dernière durée de stockage est dynamique. Vous l'utilisez si vous voulez que les objets résident sur une autre île et que vous vouliez placer des pointeurs autour de cette référence. Vous les utilisez également si vos objets sont grands et si vous souhaitez créer des tableaux de taille uniquement connus à 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 à courir lorsqu'une invocation d'opérateur nouveau appropriée 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 se termine que lorsque vous appelez delete pour eux. Si vous oubliez cela, ces objets ne finissent jamais la vie. Et les objets de classe qui définissent un constructeur déclaré par l'utilisateur n'auront pas leurs destructeurs appelés. Les objets ayant une durée de stockage dynamique nécessitent un traitement manuel de leur durée de vie et des ressources mémoire associées. Les bibliothèques existent pour en faciliter l'utilisation. Une récupération de place explicite pour des objets particuliers 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 préoccuper de l'appel à delete: le ptr partagé le fait à votre place, 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. So its durée de vie est automatiquement gérée, ce qui lui permet de vérifier s’il faut supprimer l’objet dynamique pointé dans son destructeur. Pour la référence shared_ptr, voir les documents de renforcement: http://www.boost.org/doc/libs/1_37_0/libs/smart_ptr/shared_ptr.htm
Cela a été dit avec précision, juste 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 il est instancié dans (2)
variable de tas
durée de vie = défini par vous (nouveau à supprimer)
visibilité = défini par vous (à quoi que vous assigniez le pointeur)
variable de pile
visibilité = de la déclaration jusqu'à la sortie de la portée
durée de vie = de la déclaration jusqu'à la déclaration de la portée est quitté
(1) plus exactement: de l’initialisation à la désinitialisation de l’unité de compilation (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.
Je suis sûr que l'un des pédants proposera une meilleure réponse sous peu, mais la différence principale réside dans la vitesse et la taille.
Empiler
Dramatiquement plus rapide à allouer. Cela se fait en O(1) car il est alloué lors de la configuration du cadre de pile, il est donc essentiellement libre. L'inconvénient est que si vous manquez d'espace de pile, vous êtes désossé. Vous pouvez ajustez la taille de la pile, mais IIRC vous avez ~ 2 Mo pour jouer. En outre, dès que vous quittez la fonction, tout est effacé de la pile. Il peut donc être problématique de s'y référer plus tard. bogues.)
Tas
Dramatiquement plus lent à allouer. Mais vous devez jouer avec GB et pointer dessus.
Éboueur
Le ramasse-miettes 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 qui s'appelle une fuite de mémoire. Au fil du temps, la mémoire que votre application consomme augmente de plus en plus jusqu'à ce qu'elle se bloque. Avoir un ramasse-miettes libérer 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.
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 le retour du sous-programme qui effectue l'allocation.
Je pourrais écrire une application entière sans allouer de variables dans le tas?
Peut-être, mais pas une grande application non triviale, normale (mais des programmes dits "intégrés" pourraient être écrits sans le tas, en utilisant un sous-ensemble de C++).
Quel éboueur fait?
Il surveille constamment vos données ("balayer et balayer") pour détecter le moment où votre application ne les référence plus. C'est pratique pour l'application, car elle n'a pas besoin de désallouer les données ... mais le ramasse-miettes peut être coûteux en calcul.
Les éboueurs ne sont pas une fonctionnalité habituelle de la programmation C++.
Que pourriez-vous faire en manipulant vous-même la mémoire que vous ne pouviez pas utiliser avec ce ramasse-miettes?
Apprenez les mécanismes C++ pour la désallocation de mémoire déterministe:
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 surchargez la mémoire disponible pour les affectations de pile. Le tas concerne 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 entier sans utiliser le tas.
Vous pouvez facilement perdre de la mémoire sans un ramasse-miettes, 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 quand 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 être exécuté). Donc, si les performances sont critiques et vous pouvez être sûr qu'il n'y a pas de fuite d'objets et qu'il est très utile de ne pas utiliser de CPG, sinon vous détestez la vie lorsque votre application consomme de la mémoire et vous devez rechercher la source d'une fuite.
Un avantage de GC dans certaines situations est un ennui dans d'autres; le recours à GC encourage à ne pas trop y penser. En théorie, attend jusqu'à ce que le temps d'inactivité soit écoulé ou jusqu'à ce qu'il le soit absolument, au vol de la bande passante et au temps de réponse de votre application.
Mais vous n'avez pas à "ne pas y penser". Comme pour tout ce qui se passe dans les applications multithread, vous pouvez céder lorsque vous pouvez céder. Ainsi, par exemple, en .Net, il est possible de demander un GC. En faisant cela, au lieu de faire fonctionner moins longtemps le CPG plus fréquemment, vous pouvez avoir un plus grand nombre de fois faire fonctionner le CPG plus court et répartir la latence associée à cette surcharge.
Mais cela fait échec à l'attraction principale de GC, qui semble être "encouragée à ne pas trop y penser, car elle est auto-mat-ic".
Si vous avez été initié à la programmation avant que GC devienne répandu et que vous maîtrisiez malloc/free et new/delete, il se peut même que vous trouviez GC un peu gênant et/ou que vous soyez méfiant (comme vous pourriez être méfiant), l’optimisation ", qui a une histoire en dents de scie.) 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 consiste à renoncer aux environnements de GC et à se diriger vers du code purement non géré (ou Dieu nous en préserve, un art en voie de disparition, le langage d'assemblage).
Il y a quelque temps, j'ai eu un étudiant d'été ici, un stagiaire, un enfant intelligent, qui a été sevré sur GC; il était tellement attaché à la supériorité du GC que, même lorsqu'il programmait en C/C++ non géré, il refusait de suivre le modèle malloc/free new/delete car, citation, "vous ne devriez pas avoir à le faire dans un langage de programmation moderne". Et vous savez? Vous pouvez certes vous en tirer avec de petites applications très courtes, mais pas pour des applications performantes de longue durée.
Que se passe-t-il si votre programme ne sait pas au début combien de mémoire allouer (par conséquent, vous ne pouvez pas utiliser de variables de pile)? Dites des listes chaînées, les listes peuvent grossir sans savoir d'avance quelle est sa taille. Il est donc logique d’allouer un segment de mémoire pour une liste chaînée lorsque vous ne savez pas combien d’éléments y seraient insérés.
Stack est une mémoire allouée par le compilateur, chaque fois que nous compilons le programme, le compilateur alloue par défaut de la mémoire à partir du système d'exploitation (nous pouvons modifier les paramètres à partir des paramètres du compilateur dans votre IDE) et OS est celui qui vous en donne la mémoire, cela dépend sur beaucoup de mémoire disponible sur le système et beaucoup d'autres choses, et venir empiler de la mémoire est allouer quand nous déclarons une variable qu'ils copient (ref comme formels) ces variables sont poussées dessus pour empiler elles suivent certaines conventions d'appellation par défaut son CDECL dans Visual studios ex: notation infixe: c = a + b; la pile est poussée de droite à gauche PUSHING, b pour empiler, opérateur, a pour empiler et obtenir le résultat de ces i, e c. En notation pré-assignée: = + cab Ici, toutes les variables sont placé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 utilisaient 700 Ko de mémoire (toutes les variables locales sont placées dans une pile sauf si elles sont allouées de manière dynamique), de sorte que la mémoire restante de 324 Ko est allouée à heap. Et cette pile a moins de temps de vie, lorsque la portée de la fonction se termine, ces piles sont effacées.