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.

Par
Publié le

« 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 viela fonction (le bloc)jusqu'à ton free
Qui alloue et libèrele compilateur, tout seultoi, avec malloc et free
Taillelimitée (souvent 8 Mo)grande, limitée par la RAM
Vitessetrès rapideplus 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 :

zones.c
#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;}
Terminal
$ gcc -Wall -Wextra -g zones.c -o zones$ ./zoneslocale    0xfffff2253c7ctas       0x425a2a0total     0x420048appels    0x42004ccompteur  0x420040message   0x4007c8main      0x4006c4

Range 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.

Les zones, des adresses hautes aux basses

Pile (stack)

main() (pile, adresses hautes)
intlocale
7
0xffff...3c7c
int *tas
bloc

Tas (heap)

blocinttas, 0x42...a0
5

Données statiques

compteurintglobale, 0x420040
42
message_datachar[8]littéral, lecture seule
'b''o''n''j''o''u''r''\0'

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 :

vie.cVisualiser
#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;}
Exécute ton code pas à pas dans le visualiseur

La taille : pourquoi un gros tableau local plante

La pile a une taille maximale, que ulimit -s affiche en kilo-octets :

Terminal
$ ulimit -s8192

8192 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 :

grand.cÀ éviter
#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;}
Terminal
$ gcc -Wall -Wextra -g grand.c -o grand$ ./grandSegmentation fault

Le 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 :

Terminal
$ ./grand-tasimage prête : 16777216 octets

grand-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 :

renvoi.cÀ éviter
#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;}
Terminal
$ 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 fault
Après le return, le cadre de copie() a disparu

Pile (stack)

main()
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.

C

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.

Ouvrir l'exercice
C

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.

Ouvrir l'exercice

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

À toi d'écrire le code.