☀️ Stage Pré-rentrée · dès le 24 aoûtRéserver ma place →
Majorant
📘 Fiche de cours · 1re année💻 MP2I💻 Informatique MP2I / MPINiveau · MP2I

C — Pointeurs et allocation dynamique

Le cœur du C : l'adresse et le pointeur, les opérateurs & et *, pourquoi il faut un pointeur pour modifier une variable (le passage par valeur), le lien tableaux/pointeurs, et malloc/free — chaque programme compilé et tracé, avec trois exercices corrigés.

Fiche rédigée par les mentors Majorant — alumni Polytechnique, CentraleSupélec et Mines Paris.

4 définitionsMis à jour le 2026-08-02

Vue d'ensemble

En C, une variable n'est pas une boîte abstraite : c'est un emplacement physique dans la mémoire de l'ordinateur, repéré par un numéro appelé adresse. Comprendre les pointeurs, c'est comprendre comment manipuler directement ces emplacements. C'est le cœur du C, et la raison pour laquelle une fonction peut, ou ne peut pas, modifier une variable de l'appelant.

Tant que ce chapitre n'est pas maîtrisé, le C reste flou : les tableaux, les chaînes, l'allocation dynamique, les structures chaînées reposent toutes sur les pointeurs. On avance donc lentement, une notion à la fois, en décortiquant chaque ligne.

Au programme (MP2I, informatique). Notion d'adresse mémoire ; type pointeur (int *p) ; opérateurs & (adresse de) et * (déréférencement) ; passage d'arguments par valeur et modification via un pointeur ; lien entre tableaux et pointeurs (t[i] et *(t+i)) ; allocation dynamique avec malloc et libération avec free ; pièges classiques (fuite mémoire, pointeur non initialisé, pointeur nul).

Prérequis

  • Déclarer des variables et des fonctions en C (fiche C — Premiers pas).
  • Écrire une boucle for et une condition if.
  • Connaître les types de base int, char, et la notion de tableau int t[5].
  • Utiliser printf avec les formats %d et %p.
🎯 Accompagnement Majorant

Les pointeurs, ça se débloque en une séance. Beaucoup d'élèves de MP2I calent ici, puis tout devient limpide dès qu'un mentor déroule la mémoire avec eux, case par case. Nos mentors alumni X · Centrale · Mines l'ont enseigné des dizaines de fois.

Trouver un mentor →

Adresse et pointeur

Quand tu écris int x = 42;, le compilateur réserve quelque part en mémoire une zone, de la taille d'un int (souvent 4 octets), pour y ranger la valeur 42. Cette zone a une position dans la mémoire : son adresse.

Définition 1.1 — Adresse mémoire

L'adresse d'une variable est le numéro de l'emplacement mémoire où cette variable est stockée. On l'obtient avec l'opérateur & : &x se lit « adresse de x ». C'est un nombre (affiché en hexadécimal par %p), mais on ne l'écrit jamais à la main : on laisse la machine le fournir.

Définition 1.2 — Pointeur

Un pointeur est une variable dont la valeur est une adresse. On dit qu'il « pointe vers » la variable qui se trouve à cette adresse. Le type précise vers quoi il pointe : int *p déclare un pointeur vers un int, char *c un pointeur vers un char. L'étoile fait partie du type : p n'est pas un int, c'est « un pointeur vers un int ».

Deux opérateurs, exactement inverses l'un de l'autre, font tout le travail :

  • &x — de la variable vers son adresse (« où est x ? »).
  • *p — de l'adresse vers la valeur (« que vaut ce qui est à l'adresse p ? »). On appelle cela déréférencer.
#include <stdio.h>

int main(void) {
    int x = 42;       // une variable entiere
    int *p = &x;      // p recoit l'adresse de x : p pointe vers x
    printf("%d\n", x);   // la valeur de x
    printf("%d\n", *p);  // la valeur pointee par p, donc celle de x
    *p = 99;             // on ecrit 99 a l'adresse pointee : on modifie x
    printf("%d\n", x);   // x vaut maintenant 99
    return 0;
}
🔍 Décryptage ligne par ligne
int x = 42;On crée un entier x valant 42 ; le compilateur lui réserve une adresse en mémoire.
int *p = &x;On déclare un pointeur vers un int et on l'initialise avec &x, l'adresse de x. Désormais p « pointe vers » x. Le type int * doit correspondre au type de x (un int).
printf("%d\n", x);Affiche directement la valeur de x : 42.
printf("%d\n", *p);*p déréférence : on va à l'adresse contenue dans p et on lit la valeur qui s'y trouve, soit celle de x. Affiche aussi 42.
*p = 99;Ici *p est à gauche du = : on écrit 99 à l'adresse pointée par p. Comme p pointe vers x, cela change x lui-même.
printf("%d\n", x);Affiche 99 : x a bien été modifié à travers le pointeur, sans qu'on écrive x = 99.
⚠ L'étoile a deux sens selon le contexte. Dans une déclaration (int *p), l'étoile fait partie du type et signifie « pointeur vers ». Dans une expression (*p), l'étoile est l'opérateur de déréférencement, « la valeur pointée par ». Même symbole, deux rôles : ne les confonds pas.
📝 & et * s'annulent. Pour n'importe quelle variable x, l'expression *(&x) vaut exactement x : on prend l'adresse de x, puis on lit la valeur à cette adresse, donc on retombe sur x.

Passage par valeur : l'échange qui échoue

En C, quand on appelle une fonction, ses arguments sont copiés. La fonction travaille sur des copies locales ; modifier une copie ne change rien chez l'appelant. C'est le passage par valeur, et c'est la règle par défaut pour tous les types de base.

Illustration avec la fonction canonique : échanger deux entiers. Voici la version naïve, qui ne marche pas.

#include <stdio.h>

void echange_faux(int a, int b) {
    int tmp = a;
    a = b;
    b = tmp;
}

int main(void) {
    int x = 3, y = 7;
    echange_faux(x, y);
    printf("x = %d, y = %d\n", x, y);  // x = 3, y = 7  (rien n'a change !)
    return 0;
}
🔍 Décryptage ligne par ligne
void echange_faux(int a, int b)La fonction reçoit deux int par valeur : a et b sont des copies de x et y. Elle ne renvoie rien (void).
int tmp = a;On sauvegarde la copie a dans une variable temporaire.
a = b; b = tmp;On échange bien a et b… mais ce sont les copies locales. Les vraies variables x et y de main ne sont jamais touchées.
echange_faux(x, y);À l'appel, C copie les valeurs 3 et 7 dans a et b. Au retour, ces copies sont détruites ; x et y valent toujours 3 et 7.
printf(...)Affiche x = 3, y = 7 : l'échange a échoué. C'est le piège central du passage par valeur.
⚠ Une fonction qui reçoit un int ne peut pas modifier la variable de l'appelant. Elle n'en a qu'une copie. Pour agir sur la variable originale, il faut lui donner son adresse, c'est-à-dire un pointeur.

Modifier une variable via un pointeur : l'échange qui marche

La solution : ne pas passer les valeurs, mais les adresses. La fonction reçoit alors des pointeurs et, en déréférençant, agit directement sur les variables de l'appelant.

#include <stdio.h>

void echange(int *a, int *b) {
    int tmp = *a;   // on lit la valeur pointee par a
    *a = *b;        // on ecrit dans la case pointee par a la valeur pointee par b
    *b = tmp;       // on ecrit dans la case pointee par b l'ancienne valeur
}

int main(void) {
    int x = 3, y = 7;
    printf("avant : x = %d, y = %d\n", x, y);
    echange(&x, &y);   // on passe les ADRESSES de x et y
    printf("apres : x = %d, y = %d\n", x, y);
    return 0;
}
🔍 Décryptage ligne par ligne
void echange(int *a, int *b)La fonction reçoit deux pointeurs vers des int. a contient l'adresse d'une variable, b l'adresse d'une autre. Elle pourra donc agir sur ces variables.
int tmp = *a;*a lit la valeur stockée à l'adresse a. On la sauvegarde dans tmp. Ici tmp reçoit 3.
*a = *b;À gauche *a désigne « la case pointée par a », à droite *b vaut « la valeur pointée par b ». On écrit donc 7 dans la variable pointée par a (c'est x).
*b = tmp;On écrit l'ancienne valeur (3) dans la variable pointée par b (c'est y). L'échange est complet et il a lieu sur les vraies variables.
echange(&x, &y);On passe &x et &y, les adresses de x et y. La fonction reçoit ainsi de quoi atteindre les originaux, pas des copies.
printf("apres ...")Affiche apres : x = 7, y = 3 : cette fois l'échange a bien eu lieu.
Trace de echange(&x, &y) avec au départ x = 3, y = 7. Ici a pointe vers x, b pointe vers y.
ÉtapeInstructionx (= *a)y (= *b)tmp
0état initial37
1tmp = *a;373
2*a = *b;773
3*b = tmp;7 ✓3 ✓3
📐 Méthode — Faire modifier une variable par une fonction
  1. Côté fonction : déclare le paramètre comme un pointeur (int *a au lieu de int a).
  2. Dans la fonction : pour lire la variable, utilise *a ; pour écrire dedans, mets *a à gauche du =.
  3. Côté appel : passe l'adresse de la variable avec & (par exemple echange(&x, &y)).
📝 Tu l'utilises déjà. Le & de scanf("%d", &n); n'est pas magique : scanf doit écrire dans n, il lui faut donc l'adresse de n. C'est exactement le même mécanisme que echange.

Tableaux et pointeurs

En C, tableaux et pointeurs sont intimement liés. Le nom d'un tableau, utilisé dans une expression, vaut l'adresse de sa première case. C'est pourquoi on peut passer un tableau à une fonction sans le copier : on passe en réalité un pointeur vers son début.

Définition 3.1 — Équivalence tableau / pointeur

Si t est un tableau, alors t vaut l'adresse de t[0], et pour tout indice i valide :

L'addition t + i n'ajoute pas i octets : elle avance de i cases, c'est-à-dire de i fois la taille d'un élément. C'est l'arithmétique des pointeurs.

#include <stdio.h>

int main(void) {
    int t[4] = {10, 20, 30, 40};
    printf("%d\n", t[2]);      // notation habituelle
    printf("%d\n", *(t + 2));  // notation pointeur : meme case
    printf("%d\n", *t);        // *t vaut t[0]
    return 0;
}
🔍 Décryptage ligne par ligne
int t[4] = {10, 20, 30, 40};Un tableau de 4 entiers. Les 4 cases sont contiguës en mémoire ; t désigne l'adresse de la première.
printf("%d\n", t[2]);Notation classique : la case d'indice 2, donc 30.
printf("%d\n", *(t + 2));t + 2 est l'adresse de la case 2 (on avance de 2 cases depuis le début) ; * lit sa valeur. Affiche aussi 30 : c'est exactement t[2].
printf("%d\n", *t);*t vaut *(t + 0), donc t[0], soit 10.
⚠ Un tableau connaît son adresse, pas sa taille. Une fois passé à une fonction, un tableau « n'est plus qu'un pointeur » : sizeof n'y donne plus la taille totale. Il faut donc toujours transmettre la longueur n en paramètre à côté du tableau.

Allocation dynamique : malloc et free

Un tableau int t[4] a une taille fixée à la compilation. Mais souvent on ne connaît le nombre d'éléments qu'à l'exécution (saisi par l'utilisateur, lu dans un fichier…). Il faut alors allouer dynamiquement la mémoire, avec malloc, et la rendre quand on a fini, avec free.

Définition 4.1 — Allocation dynamique

malloc(taille) (de memory allocation, déclaré dans <stdlib.h>) réserve un bloc de taille octets et renvoie l'adresse du début de ce bloc, ou NULL si la réservation échoue. Pour un tableau de n entiers, on demande n * sizeof(int) octets. free(p) rend au système le bloc dont p est le début.

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int n = 5;
    int *t = malloc(n * sizeof(int));  // un tableau de n entiers
    if (t == NULL) return 1;           // securite : malloc a-t-il reussi ?
    for (int i = 0; i < n; i++) {
        t[i] = i * i;                  // 0, 1, 4, 9, 16
    }
    int somme = 0;
    for (int i = 0; i < n; i++) {
        somme += t[i];
    }
    printf("somme = %d\n", somme);     // 0+1+4+9+16 = 30
    free(t);                           // on rend la memoire
    return 0;
}
🔍 Décryptage ligne par ligne
int *t = malloc(n * sizeof(int));On demande de quoi stocker n entiers, soit n * sizeof(int) octets. malloc renvoie l'adresse du bloc ; on la range dans le pointeur t. On peut ensuite utiliser t comme un tableau.
if (t == NULL) return 1;Si la mémoire manque, malloc renvoie NULL. Toujours tester : déréférencer NULL fait planter le programme.
for (int i = 0; i < n; i++) { t[i] = i * i; }On remplit les cases. t[i] fonctionne exactement comme pour un tableau statique, grâce à l'équivalence t[i]*(t+i).
somme += t[i];Seconde boucle : on additionne les cases. La somme des carrés 0+1+4+9+16 vaut 30.
printf("somme = %d\n", somme);Affiche somme = 30.
free(t);On rend le bloc au système. Sans ce free, la mémoire reste réservée jusqu'à la fin du programme : c'est une fuite mémoire.
⚠ Fuite mémoire : chaque malloc réclame son free. Oublier free laisse le bloc réservé pour rien. Dans une boucle qui alloue à répétition sans libérer, la consommation grimpe jusqu'au plantage. Règle mnémotechnique : un malloc, un free.
⚠ Ne touche pas à un bloc après free, ni deux fois free. Après free(t), l'adresse dans t n'est plus valable : y lire ou y écrire, ou refaire free(t), est un comportement indéfini. Bonne habitude : t = NULL; juste après le free.
💡 Pointeur non initialisé. Écrire int *p; *p = 5; sans jamais avoir donné d'adresse valable à p écrit à une adresse au hasard : plantage probable. Un pointeur doit toujours pointer vers quelque chose de valable (une variable via &, ou un bloc malloc) avant d'être déréférencé.
🎯 Accompagnement Majorant

Segfault, fuite, double free : les erreurs mémoire font perdre des heures. Un mentor t'apprend à les lire, à les prévenir, et à raisonner sur la mémoire comme un pro. Nos mentors alumni X · Centrale · Mines sont passés par là.

Trouver un mentor →

Récap final — Ce qu'il faut absolument retenir

Les pointeurs manipulent des adresses. Deux opérateurs inverses (& pour prendre l'adresse, * pour lire ou écrire la valeur pointée), une règle d'or (le C passe par valeur, donc pour modifier il faut passer un pointeur), et une discipline mémoire (un malloc, un free). Vérifie que tu peux répondre oui à chaque question.

  • Sais-tu expliquer la différence entre une variable, sa valeur et son adresse ?
  • Sais-tu ce que déclare int *p, et ce que valent &x et *p ?
  • Sais-tu pourquoi echange_faux(int a, int b) ne modifie rien, alors que echange(int *a, int *b) fonctionne ?
  • Sais-tu écrire une fonction qui modifie une variable de l'appelant (paramètre pointeur puis appel avec &) ?
  • Sais-tu justifier l'équivalence t[i]*(t + i) et dire ce que vaut *t ?
  • Sais-tu allouer un tableau de n entiers avec malloc(n * sizeof(int)) et tester le retour ?
  • Sais-tu ce qu'est une fuite mémoire et où placer le free ?
  • Sais-tu pourquoi il ne faut jamais déréférencer un pointeur non initialisé ou NULL ?

Exercices corrigés

Exo 1Triple via pointeurFacile

Écris une fonction void triple(int *p) qui multiplie par 3 la variable pointée par p. Montre un main qui déclare int n = 4;, appelle triple, puis affiche n. Qu'affiche le programme ?

Voir la correction détaillée

La fonction reçoit un pointeur. Pour multiplier la variable par 3, on lit sa valeur avec *p, on multiplie, et on réécrit dans *p :

#include <stdio.h>

void triple(int *p) {
    *p = *p * 3;   // on ecrit 3 * (valeur pointee) a l'adresse p
}

int main(void) {
    int n = 4;
    triple(&n);          // on passe l'ADRESSE de n
    printf("%d\n", n);   // affiche 12
    return 0;
}

À l'appel, p reçoit &n, donc *p désigne n. La ligne *p = *p * 3; calcule 4 * 3 = 12 et l'écrit dans n. Le programme affiche 12. Si on avait écrit void triple(int p) avec p = p * 3;, on n'aurait modifié qu'une copie et n serait resté 4.

Exo 2Somme d'un tableau par pointeursIntermédiaire

Écris une fonction int somme(int *t, int n) qui renvoie la somme des n premières cases du tableau t, en utilisant l'écriture pointeur *(t + i) plutôt que t[i]. Teste-la sur {5, 1, 4, 2}.

Voir la correction détaillée

Le paramètre t est un pointeur vers le début du tableau. On parcourt les cases avec l'arithmétique des pointeurs :

#include <stdio.h>

int somme(int *t, int n) {
    int s = 0;
    for (int i = 0; i < n; i++) {
        s += *(t + i);   // equivaut a t[i]
    }
    return s;
}

int main(void) {
    int tab[4] = {5, 1, 4, 2};
    printf("%d\n", somme(tab, 4));   // affiche 12
    return 0;
}

*(t + i) lit la case d'indice i : c'est exactement t[i]. La somme 5 + 1 + 4 + 2 = 12. Noter qu'à l'appel somme(tab, 4) on passe tab (l'adresse de sa première case) sans & : le nom d'un tableau vaut déjà cette adresse. On doit transmettre 4 car la fonction ne connaît pas la taille du tableau.

Exo 3Copie allouée dynamiquementDifficile

Écris une fonction int *copie(int *t, int n) qui alloue un nouveau tableau de n entiers, y recopie les n cases de t, et renvoie l'adresse du nouveau tableau. Qui doit faire le free, et pourquoi la fonction ne peut-elle pas le faire elle-même ?

Voir la correction détaillée

On alloue avec malloc, on teste le retour, on recopie, on renvoie le pointeur :

#include <stdio.h>
#include <stdlib.h>

int *copie(int *t, int n) {
    int *c = malloc(n * sizeof(int));
    if (c == NULL) return NULL;      // echec d'allocation
    for (int i = 0; i < n; i++) {
        c[i] = t[i];                 // recopie case par case
    }
    return c;                        // on renvoie l'adresse du nouveau bloc
}

int main(void) {
    int tab[3] = {7, 8, 9};
    int *d = copie(tab, 3);
    printf("%d %d %d\n", d[0], d[1], d[2]);  // 7 8 9
    free(d);                         // c'est a l'APPELANT de liberer
    return 0;
}

La fonction renvoie l'adresse du bloc malloc. Elle ne peut pas faire free(c) avant de renvoyer : elle rendrait la mémoire, et l'appelant recevrait une adresse invalide (bloc libéré). C'est donc l'appelant qui possède le bloc et doit appeler free(d) quand il a fini. C'est la règle du C : celui qui reçoit un pointeur issu d'un malloc est responsable de sa libération. Sortie du programme : 7 8 9.

Débloque la fiche complète

Théorèmes, démonstrations à savoir refaire, méthodes-types et pièges de concours : crée ton compte gratuit pour tout lire. Une seule fois pour toutes les fiches et ressources Majorant.

Gratuit · vos données restent confidentielles.

Valide tes acquis

Quiz — C : pointeurs

11 questions · une à la fois · seuil de maîtrise 80 %.

MP2I / MPI · MP2IQuiz — C — Pointeurs et allocation dynamiqueQuestion 1 / 11
FacileChoix unique1 pt

En C, que déclare l'instruction int *p ;

Sélectionne une réponse pour valider.

Fiches associées

💻 MP2I·Informatique

C — Premiers pas

Écrire son premier programme C : la structure main/return, les types de base, printf et ses formats, les boucles for/while, et le piège n°1 — la division entière (7/2 vaut 3, pas 3,5) — chaque programme compilé et tracé, avec trois exercices corrigés.

💻 MP2I·Informatique

OCaml — Premiers pas

Découvrir OCaml, le langage fonctionnel de MP2I : le let et le typage inféré, les fonctions, le piège des opérateurs pointés (+. pour les float), le if/then/else qui renvoie une valeur, et la récursivité let rec (factorielle déroulée) — avec trois exercices corrigés.

💻 MP2I·Informatique

OCaml — Filtrage et listes

Les deux piliers d'OCaml : le filtrage (match ... with) et les listes récursives (:: et []), avec longueur et somme déroulées sur un exemple, les types somme et le type option (Some/None) — attention à l'ordre et à l'exhaustivité des cas, avec trois exercices corrigés.

💻 MP2I·Informatique

C — Structures et listes chaînées

La première structure de données dynamique du programme : les struct, le maillon et l'opérateur flèche p->suivant, l'insertion en tête et le parcours d'une liste chaînée — construction de [3, 5, 8] tracée, avec les pièges (NULL, ordre inversé, fuite mémoire) et trois exercices corrigés.

💻 MP2I·Informatique

C — Piles et files

Les deux structures linéaires fondamentales implémentées en C : la pile (LIFO, empiler/dépiler en tête) et la file (FIFO, avec un pointeur de queue pour enfiler en O(1)) — chaque opération compilée et tracée, avec trois exercices corrigés.

💻 MP2I·Informatique

OCaml — Arbres binaires

L'arbre binaire comme type somme récursif OCaml (Vide | Noeud) : taille, hauteur et parcours infixe écrits par filtrage sur Vide / Noeud(g,x,d), déroulés à la main sur un petit arbre — chaque cas du type devient un cas du match, avec trois exercices corrigés.

Tu veux aller plus loin sur ce chapitre ?

Nos mentors alumni de Polytechnique, CentraleSupélec et Mines Paris t'accompagnent en cours particuliers — démonstrations détaillées, exos type concours, oraux blancs.

Trouver un mentor →