Pile et tas en C : différence, durée de vie, taille
Pile et tas en C : la différence de durée de vie, de taille et de vitesse, où vivent globales et littéraux, et pourquoi renvoyer une adresse de pile est un bug.
« Mets ça sur le tas », « cette variable est sur la pile » : les deux mots reviennent sans cesse quand on écrit du C, souvent sans qu'on sache vraiment ce qui les sépare. La différence est concrète, et elle décide de la durée de vie de tes données, de la taille maximale que tu peux leur donner, et de qui doit les libérer.
En bref
La pile (stack) et le tas (heap) sont deux zones de la mémoire d'un programme. La pile stocke les variables locales de chaque fonction : elle est rapide, limitée (souvent 8 Mo), et le compilateur la gère tout seul, chaque variable disparaît à la fin de son bloc. Le tas sert aux blocs que tu demandes avec malloc : il est grand, mais c'est à toi de le libérer avec free, et un bloc y survit tant que tu ne le rends pas. Les variables globales et les chaînes littérales ne sont ni sur l'une ni sur l'autre : elles vivent dans une zone statique, allouée une fois pour toute la durée du programme.
Pile et tas : la différence en un tableau
Quatre critères suffisent à les distinguer.
| Pile (stack) | Tas (heap) | |
|---|---|---|
| Durée de vie | la fonction (le bloc) | jusqu'à ton free |
| Qui alloue et libère | le compilateur, tout seul | toi, avec malloc et free |
| Taille | limitée (souvent 8 Mo) | grande, limitée par la RAM |
| Vitesse | très rapide | plus lente (recherche d'un bloc) |
La pile avance et recule au rythme des appels de fonction : entrer dans une fonction réserve un cadre (ses variables locales), en sortir le rend d'un coup. C'est pour ça qu'elle est rapide, et qu'une variable locale n'existe plus dès que sa fonction se termine. Le tas, lui, ne suit pas les appels : un bloc que tu alloues dans une fonction peut être utilisé bien plus tard, ailleurs, et ne part que quand tu le libères.
Où vit chaque variable
Le plus simple est de demander leur adresse aux variables elles-mêmes. Ce programme en déclare une de chaque sorte et affiche son adresse avec %p :
#include <stdio.h>#include <stdlib.h>
int compteur = 42; /* globale initialisée */int total; /* globale non initialisée */
int main(void){ int locale = 7; /* sur la pile */ static int appels = 0; /* locale, mais statique */ char *message = "bonjour"; /* littéral en lecture seule */ int *tas = malloc(sizeof(int)); /* bloc sur le tas */
if (tas == NULL) return 1; *tas = 5; printf("locale %p\n", (void *)&locale); printf("tas %p\n", (void *)tas); printf("total %p\n", (void *)&total); printf("appels %p\n", (void *)&appels); printf("compteur %p\n", (void *)&compteur); printf("message %p\n", (void *)message); printf("main %p\n", (void *)main); free(tas); return 0;}$ gcc -Wall -Wextra -g zones.c -o zones$ ./zoneslocale 0xfffff2253c7ctas 0x425a2a0total 0x420048appels 0x42004ccompteur 0x420040message 0x4007c8main 0x4006c4Range ces adresses par ordre de grandeur et la carte de la mémoire apparaît. Tout en haut, locale, vers 0xfffff... : c'est la pile, qui part des adresses hautes. Beaucoup plus bas, tas, vers 0x42...a0 : le tas, juste au-dessus de la zone statique. Puis un petit groupe serré, total, appels et compteur, autour de 0x420040 : la zone statique, là où vivent les globales et les variables static. Enfin message et main, vers 0x400... : la chaîne littérale et le code, dans la partie en lecture seule du programme.
Pile (stack)
- intlocale
- 7
- 0xffff...3c7c
- int *tas
- bloc
Tas (heap)
Données statiques
Deux relances de suite montrent un détail utile : les adresses de la pile et du tas changent à chaque exécution (c'est l'ASLR, qui déplace ces zones pour la sécurité), alors que celles de la zone statique et du code restent identiques.
Pour suivre ce découpage pas à pas sur un vrai programme, le visualiseur de Grademe montre la pile, le tas et les pointeurs qui les relient à chaque ligne :
#include <stdlib.h>
int global = 42;
int main(void){ int locale = 7; int *copie = malloc(sizeof(int));
if (copie == NULL) return 1; *copie = locale; free(copie); return 0;}La taille : pourquoi un gros tableau local plante
La pile a une taille maximale, que ulimit -s affiche en kilo-octets :
$ ulimit -s81928192 Ko, soit 8 Mo. C'est confortable pour des variables ordinaires, mais pas pour un gros tableau local. Ce programme déclare un tableau de 16 Mo sur la pile :
#include <stdio.h>#include <string.h>
int main(void){ char image[16 * 1024 * 1024];
memset(image, 0, sizeof(image)); printf("image prête : %zu octets\n", sizeof(image)); return 0;}$ gcc -Wall -Wextra -g grand.c -o grand$ ./grandSegmentation faultLe tableau dépasse les 8 Mo de la pile, le premier accès au-delà de la limite déclenche le signal. La même donnée sur le tas passe sans problème, parce que le tas n'a pas cette limite étroite :
$ ./grand-tasimage prête : 16777216 octetsgrand-tas.c est le même programme avec un malloc(16 * 1024 * 1024) à la place du tableau local. La règle pratique : dès qu'une donnée est grosse ou que sa taille n'est connue qu'à l'exécution, c'est le tas qu'il faut, pas la pile.
Renvoyer une adresse de pile : le bug classique
Puisqu'une variable locale disparaît à la fin de sa fonction, renvoyer son adresse donne un pointeur vers une case qui ne t'appartient plus. C'est une des causes les plus fréquentes de segmentation fault, et GCC la détecte souvent :
#include <stdio.h>#include <string.h>
char *copie(const char *source){ char tampon[32];
strcpy(tampon, source); return tampon; /* tampon meurt ici */}
int main(void){ char *s = copie("bonjour");
printf("%s\n", s); return 0;}$ gcc -Wall -Wextra -g renvoi.c -o renvoirenvoi.c: In function 'copie':renvoi.c:9:12: warning: function returns address of local variable [-Wreturn-local-addr] 9 | return tampon; /* tampon meurt ici */ | ^~~~~~$ ./renvoiSegmentation faultPile (stack)
- char *s
- ? indéterminé
- vise le cadre rendu de copie()
Le pointeur s dans main vise une zone de pile déjà rendue : l'utiliser est un comportement indéfini. La solution est de mettre le résultat sur le tas, avec malloc, et de laisser l'appelant le libérer. Les deux exercices ci-dessous travaillent les deux côtés de cette frontière : l'un distribue la mémoire à la main, l'autre rend visible la pile des cadres.
arena_alloc
Écris un allocateur « bump » qui sert des tranches alignées sur 8 depuis un pool static de 4096 octets, sans jamais appeler malloc. Tu manipules à la main la frontière entre ce qui est donné et ce qui reste libre : la meilleure façon de comprendre ce qu'un allocateur fait au tas.
descend
descend(n, ...) s'appelle de n jusqu'à 1 en signalant chaque niveau à la descente puis à la remontée. Les appels qui remontent sortent dans l'ordre inverse de ceux qui descendent : c'est la pile des cadres de fonction, rendue visible.
Questions fréquentes
Quelle est la différence entre la pile et le tas ?
La pile stocke les variables locales et est gérée par le compilateur : chaque variable y naît à l'entrée de sa fonction et meurt à sa sortie. Le tas contient les blocs que tu demandes avec malloc : ils vivent jusqu'à ton free, indépendamment des fonctions. La pile est rapide et petite, le tas est grand et à ta charge.
Les variables globales sont-elles sur le tas ?
Non. Les globales et les variables static ont une durée de vie statique : elles occupent une zone à part, allouée au démarrage pour toute l'exécution. Le tas ne contient que ce que malloc, calloc ou realloc y placent.
Pourquoi un grand tableau local fait-il planter le programme ?
Parce que la pile a une taille maximale, souvent 8 Mo (ulimit -s). Un tableau local plus grand la dépasse, et le premier accès au-delà déclenche un segmentation fault. Alloue cette donnée sur le tas avec malloc.
Où sont stockées les chaînes littérales ?
Dans une zone en lecture seule du programme, avec le code, pas sur la pile ni sur le tas. C'est pourquoi char *s = "texte"; s[0] = 'X'; plante : tu écris dans une zone qu'on ne peut que lire. Pour une chaîne modifiable, déclare un tableau : char s[] = "texte";.
Dois-je toujours préférer le tas pour être sûr ?
Non. La pile est plus rapide et se libère toute seule, c'est le bon choix pour les données de taille connue et modérée. Réserve le tas aux données grosses, de taille variable, ou qui doivent survivre à la fonction qui les crée.
Sources
