Comment fonctionne l’allocation automatique de mémoire en C ++?

En C ++, en l’absence d’optimisation, les deux programmes suivants se retrouvent-ils avec le même code machine d’allocation de mémoire?

int main() { int i; int *p; } int main() { int *p = new int; delete p; } 

Non, sans optimisation …

 int main() { int i; int *p; } 

ne fait presque rien – juste quelques instructions pour ajuster le pointeur de la stack, mais

 int main() { int *p = new int; delete p; } 

alloue un bloc de mémoire sur le tas, puis le libère, ce qui représente beaucoup de travail (je suis sérieux, l’allocation de tas n’est pas une opération sortingviale).

Pour mieux comprendre ce qui se passe, imaginons que nous n’ayons qu’un système d’exploitation très primitif fonctionnant sur un processeur 16 bits ne pouvant exécuter qu’un processus à la fois. C’est-à-dire qu’un seul programme peut être exécuté à la fois. De plus, supposons que toutes les interruptions sont désactivées.

Il y a une construction dans notre processeur appelée la stack. La stack est une construction logique imposée à la mémoire physique. Disons que notre RAM existe aux adresses E000 à FFFF. Cela signifie que notre programme en cours peut utiliser cette mémoire comme bon nous semble. Imaginons que notre système d’exploitation indique qu’E000 to EFFF est la stack et F000 to FFFF est le tas.

La stack est gérée par le matériel et par les instructions de la machine. Il n’y a vraiment pas grand chose à faire pour le maintenir. Tout ce que nous (ou notre système d’exploitation) devons faire est de nous assurer de définir une adresse appropriée pour le début de la stack. Le pointeur de stack est une entité physique résidant dans le matériel (processeur) et est géré par des instructions de processeur. Dans ce cas, notre pointeur de stack serait défini sur EFFF (en supposant que la stack croît BACKWARDS, ce qui est assez commun, -). Avec un langage compilé comme C, lorsque vous appelez une fonction, tous les arguments que vous avez transmis à la fonction sur la stack sont répercutés. Chaque argument a une certaine taille. int est généralement de 16 ou 32 bits, char est de 8 bits, etc. Supposons que sur notre système, int et int * sont de 16 bits. Pour chaque argument, le pointeur de la stack est DECREMENTED (-) par sizeof (argument), et l’argument est copié dans la stack. Ensuite, toutes les variables que vous avez déclarées dans la scope sont placées de la même manière dans la stack, mais leurs valeurs ne sont pas initialisées.

Reprenons deux exemples similaires à vos deux exemples.

 int hello(int eeep) { int i; int *p; } 

Ce qui se passe ici sur notre système 16 bits est le suivant: 1) poussez eeep sur la stack. Cela signifie que nous décrémentons le pointeur de stack sur EFFD (parce que sizeof (int) vaut 2), puis copions en fait l’adresse eeep dans l’adresse EFFE (la valeur actuelle de notre pointeur de stack, moins 1, car notre pointeur de stack pointe vers le premier emplacement disponible). après l’atsortingbution). Parfois, certaines instructions peuvent faire les deux en même temps (en supposant que vous copiez des données qui entrent dans un registre. Dans le cas contraire, vous devrez copier manuellement chaque élément d’un type de données à sa place dans la stack – l’ordre est important! ).

2) créer un espace pour i. Cela signifie probablement simplement que le pointeur de la stack est décrémenté à EFFB.

3) créer un espace pour p. Cela signifie probablement simplement que le pointeur de stack est décrémenté sur EFF9.

Ensuite, notre programme s’exécute, en mémorisant l’emplacement de nos variables (eeep commence à EFFE, i à EFFC et p à EFFA). La chose importante à retenir est que même si la stack compte BACKWARDS, les variables fonctionnent toujours FORWARDS (cela dépend en fait de endianness, mais le fait est que & eeep == EFFE, pas EFFF).

Lorsque la fonction se ferme, nous incrémentons simplement (++) le pointeur de stack de 6 (car 3 “objects”, pas le type c ++, de taille 2 ont été placés sur la stack.

Maintenant, votre deuxième scénario est beaucoup plus difficile à expliquer car il existe tellement de méthodes pour le réaliser qu’il est presque impossible de l’expliquer sur Internet.

 int hello(int eeep) { int *p = malloc(sizeof(int));//C's pseudo-equivalent of new free(p);//C's pseudo-equivalent of delete } 

eeep et p sont toujours poussés et alloués sur la stack comme dans l’exemple précédent. Dans ce cas, cependant, nous initialisons p au résultat d’un appel de fonction. Ce que malloc (ou le nouveau, mais le nouveau en fait plus en c ++. Il appelle des constructeurs lorsque cela est approprié, et tout le rest.) Fait: il va à cette boîte noire appelée HEAP et obtient une adresse de mémoire libre. Notre système d’exploitation gérera le tas pour nous, mais nous devons le laisser savoir quand nous voulons de la mémoire et quand nous en avons fini.

Dans l’exemple, lorsque nous appelons malloc (), le système d’exploitation renvoie un bloc de 2 octets (sizeof (int) sur notre système est égal à 2) en nous donnant l’adresse de départ de ces octets. Disons que le premier appel nous a donné l’adresse F000. Le système d’exploitation garde ensuite une trace du fait que les adresses F000 et F001 sont actuellement utilisées. Lorsque nous appelons free (p), le système d’exploitation trouve le bloc de mémoire pointé par p et marque 2 octets comme étant inutilisés (car sizeof (star p) vaut 2). Si au lieu de cela nous allouons plus de mémoire, l’adresse F002 sera probablement retournée comme bloc de départ de la nouvelle mémoire. Notez que malloc () est une fonction. Quand p est poussé sur la stack pour l’appel de malloc (), il est copié de nouveau sur la stack à la première adresse ouverte qui a suffisamment d’espace sur la stack pour contenir la taille de p (probablement EFFB, car nous n’avons poussé que 2 les objects de la stack cette fois de taille 2 et sizeof (p) est égal à 2), le pointeur de stack est à nouveau décrémenté en EFF9 et malloc () placera ses variables locales sur la stack à partir de cet emplacement. Une fois que malloc a terminé, il supprime tous ses éléments de la stack et définit le pointeur de la stack sur l’état antérieur à son appel. La valeur de retour de malloc (), une écanvas vide, sera probablement placée dans un registre (généralement l’accumulateur de nombreux systèmes) à notre usage.

Dans la mise en œuvre, les deux exemples ne sont VRAIMENT pas aussi simples. Lorsque vous allouez de la mémoire de stack, pour un nouvel appel de fonction, vous devez vous assurer que vous enregistrez votre état (enregistrez tous les registres) afin que la nouvelle fonction ne supprime pas les valeurs de manière permanente. Cela implique généralement de les pousser sur la stack. De la même manière, vous sauvegarderez généralement le registre des compteurs de programme afin de pouvoir retourner au bon endroit après le retour du sous-programme. Les gestionnaires de mémoire utilisent leur propre mémoire pour “se souvenir” de ce qui a été dissortingbué et de ce qui ne l’a pas été. La mémoire virtuelle et la segmentation de la mémoire compliquent d’autant plus ce processus, et les algorithmes de gestion de la mémoire doivent continuellement déplacer des blocs (et les protéger également) afin d’empêcher la fragmentation de la mémoire (un sujet à part entière), ce qui est lié à la mémoire virtuelle. ainsi que. Le 2ème exemple est vraiment une grosse boîte de Pandore comparé au premier exemple. De plus, l’exécution de plusieurs processus rend tout cela beaucoup plus compliqué, car chaque processus a sa propre stack, et le tas peut être accédé par plusieurs processus (ce qui signifie qu’il doit se protéger lui-même). De plus, chaque architecture de processeur est différente. Certaines architectures s’attendent à ce que vous définissiez le pointeur de stack sur la première adresse disponible de la stack, tandis que d’autres s’attendent à ce que vous le dirigiez vers le premier emplacement non libre.

J’espère que cela a aidé. s’il vous plaît, faites-moi savoir.

remarque, tous les exemples ci-dessus sont pour une machine fictive qui est trop simplifiée. Sur du vrai matériel, cela devient un peu plus poilu.

edit: les astérisques ne s’affichent pas. je les ai remplacés par le mot “star”


Pour ce que cela vaut, si nous utilisons (principalement) le même code dans les exemples, en remplaçant respectivement “hello” par “example1” et “example2”, nous obtenons la sortie asembly suivante pour intel on wndows.

     .file "test1.c"
     .texte
 .globl _example1
     .def _example1;  .scl 2;  .type 32;  .endef
 _Exemple 1:
     pushl% ebp
     movl% esp,% ebp
     sous $ 8,% esp
     laisser
     ret
 .globl _example2
     .def _example2;  .scl 2;  .type 32;  .endef
 _exemple2:
     pushl% ebp
     movl% esp,% ebp
     sous $ 8,% esp
     movl $ 4, (% esp)
     appeler _malloc
     movl% eax, -4 (% ebp)
     movl -4 (% pbp),% eax
     movl% eax, (% esp)
     appelez _free
     laisser
     ret
     .def _free;  .scl 3;  .type 32;  .endef
     .def _malloc;  .scl 3;  .type 32;  .endef
  int i; int *p; 

^ Affectation d’un entier et d’un pointeur entier sur la stack

 int *p = new int; delete p; 

^ Affectation d’un pointeur entier sur la stack et bloc de la taille d’un entier sur le tas

MODIFIER:

Différence entre le segment de stack et le segment de tas

alt text http://soffr.miximages.com/c%2B%2B/HeapAndStack.png

 void another_function(){ int var1_in_other_function; /* Stack- main-y-sr-another_function-var1_in_other_function */ int var2_in_other_function;/* Stack- main-y-sr-another_function-var1_in_other_function-var2_in_other_function */ } int main() { /* Stack- main */ int y; /* Stack- main-y */ char str; /* Stack- main-y-sr */ another_function(); /*Stack- main-y-sr-another_function*/ return 1 ; /* Stack- main-y-sr */ //stack will be empty after this statement } 

Chaque fois qu’un programme commence à s’exécuter, il stocke toutes ses variables dans un emplacement mémoire spécial appelé segment de stack . Par exemple, dans le cas de C / C ++, la première fonction appelée est main. donc il sera mis sur la stack en premier. Toutes les variables à l’intérieur de main seront mises sur la stack pendant l’exécution du programme. Maintenant, comme main est la première fonction appelée, ce sera la dernière fonction à renvoyer une valeur (ou sera extraite de la stack).

Désormais, lorsque vous allouez dynamicment de la mémoire à l’aide d’un new emplacement, un autre emplacement de mémoire spécial appelé segment de segment de mémoire est utilisé. Même si les données réelles sont présentes sur le pointeur de tas se trouvent sur la stack.

On dirait que vous ne connaissez pas la stack et le tas. Votre premier exemple consiste simplement à allouer de la mémoire sur la stack, qui sera supprimée dès qu’elle sortira du domaine. La mémoire sur le tas obtenue à l’aide de malloc / new restra en place jusqu’à ce que vous la supprimiez à l’aide de free / delete.

Dans le premier programme, vos variables résident toutes dans la stack. Vous n’allouez aucune mémoire dynamic. ‘p’ est juste assis sur la stack et si vous déréférenciez cela, vous aurez des déchets. Dans le deuxième programme, vous créez en fait une valeur entière sur le tas. “p” pointe en fait sur une mémoire valide dans ce cas. Vous pouvez réellement déréférencer p et lui donner une signification en toute sécurité:

 *p = 5; 

Ceci est valable dans le deuxième programme (avant la suppression), pas dans le premier. J’espère que cela pourra aider.