Allocation dynamiqueDans le dialogue d’impression, choisissez « Enregistrer au format PDF » comme destination.
Retour

Programmation en C · C4 Pointeurs et mémoire · Chapitre 2 · 6 h

Allocation dynamique

Pile et tas, malloc, calloc, realloc et free, durée de vie des données, fuites et pointeurs pendants, tableaux dynamiques et liste chaînée.

Un programme lit un nombre au clavier, puis doit stocker autant de valeurs. Avec les outils du bloc III, c'est impossible : la taille d'un tableau est fixée à la compilation, et l'on ne peut que surdimensionner au hasard — int T[1000], en espérant que mille suffira et en gaspillant si l'utilisateur en saisit trois.

La tentation suivante est pire :

int *creer(int n) {    int T[n];    return T;                    /* l'adresse d'une variable LOCALE */}

Le tableau vit dans le cadre d'appel, qui est détruit au retour. La fonction rend l'adresse d'une case qui n'existe plus — un pointeur pendant au sens du chapitre 7. Le programme compile, souvent s'exécute, et corrompt sa mémoire.

Ce chapitre donne la réponse correcte : allouer dans une zone dont on décide soi-même de la durée de vie.

Deux zones, deux régimes

Le chapitre 3 du cours de systèmes a décrit l'image mémoire d'un processus. Deux de ses régions nous intéressent.

  ┌────────────────────┐  │  pile (stack)      │  variables locales, paramètres, adresses de retour  │        ↓           │  gérée AUTOMATIQUEMENT : allouée à l'entrée d'un  │                    │  bloc, libérée à sa sortie  │        ↑           │  │  tas (heap)        │  allocation dynamique  └────────────────────┘  gérée À LA MAIN : vous allouez, vous libérez
PileTas
Allocationautomatiquemalloc
Libérationautomatique, à la sortie du blocfree, par vous
Durée de viecelle du blocjusqu'au free
Tailleconnue à la compilationdécidée à l'exécution
Capacitéquelques mégaoctetsla mémoire disponible
Vitessetrès rapide (déplacer un pointeur)plus lente (chercher un bloc libre)

La ligne décisive est la troisième. Sur le tas, la donnée survit à la fonction qui l'a créée — c'est précisément ce qu'il fallait, et c'est ce que la pile ne peut pas offrir.

Le prix est dans la deuxième ligne, et c'est tout le chapitre : ce que vous allouez, vous devez le rendre.

Les quatre fonctions

#include <stdlib.h> int *T = malloc(n * sizeof(int));      /* n int, contenu INDÉFINI */int *U = calloc(n, sizeof(int));       /* n int, tous mis à ZÉRO */T = realloc(T, m * sizeof(int));       /* redimensionne à m int */free(T);                                /* rend la mémoire */

Quatre remarques, une par ligne.

malloc ne connaît pas les types : elle prend un nombre d'octets et rend un pointeur générique. D'où l'idiome n * sizeof(int), et la variante préférable n * sizeof(*T) — qui reste correcte si le type de T change un jour.

Son contenu est indéfini, comme une variable locale. calloc met à zéro, ce qui coûte un peu et évite une classe d'erreurs ; on la préfère dès que le zéro a un sens.

realloc peut déplacer le bloc. S'il n'y a pas la place de l'agrandir sur place, elle en alloue un autre ailleurs, recopie, et libère l'ancien : tous les pointeurs vers l'ancien emplacement deviennent pendants. Elle a aussi un piège d'écriture — T = realloc(T, ...) perd le pointeur original si l'allocation échoue et que NULL est rendu. On écrit donc dans une variable temporaire, qu'on n'affecte à T qu'après vérification.

malloc peut échouer et rendre NULL. Le tester n'est pas une politesse : déréférencer le résultat sans vérifier transforme une pénurie de mémoire — situation gérable — en erreur de segmentation.

int *T = malloc(n * sizeof(*T));if (T == NULL) { fprintf(stderr, "mémoire insuffisante\n"); return 1; }

Qui libère ?

C'est la vraie difficulté du chapitre, et elle n'est pas technique.

Le C n'a pas de ramasse-miettes : la propriété d'un bloc est une convention, portée par la documentation et rien d'autre. Une fonction qui rend un pointeur alloué doit dire, en toutes lettres, que l'appelant devra le libérer. Une fonction qui reçoit un pointeur doit dire si elle le libère ou non.

/* Rend une chaîne allouée : à libérer par l'appelant. */char *dupliquer(const char *source);

Trois règles de discipline évitent l'essentiel des dégâts.

Un malloc, un free, et si possible dans la même fonction ou dans la fonction jumelle — creerPile et detruirePile. Une allocation dont la libération est ailleurs, dans un autre fichier, écrit par quelqu'un d'autre, est une fuite en puissance.

Libérer dans l'ordre inverse de la construction, surtout pour les structures imbriquées : libérer une liste chaînée demande de garder le pointeur suivant avant de libérer la cellule, faute de quoi on le lit dans un bloc déjà rendu.

Mettre à NULL après free. free(p) ne modifie pas p : il rend la mémoire, et p continue de pointer dessus — pointeur pendant. Poser p = NULL transforme une utilisation ultérieure en erreur de segmentation immédiate, donc diagnosticable, et rend le double free inoffensif puisque free(NULL) ne fait rien.

Les quatre fautes

Elles portent des noms, et le chapitre 10 donnera l'outil qui les détecte.

La fuite (memory leak) : un bloc alloué que plus aucun pointeur ne désigne. Il reste occupé jusqu'à la fin du programme. Sans conséquence sur un utilitaire qui s'arrête, fatal sur un service qui tourne des mois.

Le pointeur pendant : utiliser un bloc après free. Le comportement dépend de ce que l'allocateur a fait de la place entre-temps — d'où des bogues qui apparaissent et disparaissent selon la charge.

Le double free : libérer deux fois le même bloc corrompt les structures internes de l'allocateur, et le plantage survient bien plus tard, ailleurs.

Le débordement de tas : écrire au-delà du bloc alloué. Même mécanisme qu'au chapitre 5, mais sur le tas, où l'on écrase les métadonnées de l'allocateur.

Le point commun des quatre : la faute et le symptôme sont éloignés. C'est précisément pourquoi valgrind existe, et pourquoi le chapitre 10 lui est en partie consacré.

Quiz · 1 question

Pourquoi écrit-on p = NULL juste après free(p) ?

  • Pour indiquer à l'allocateur que le bloc est libre, sans quoi la mémoire n'est pas réellement renduepour l'allocateur
  • Parce que free ne modifie pas p : il rend la mémoire mais p continue de pointer dessus. Le mettre à NULL transforme une utilisation ultérieure en erreur immédiate, donc diagnosticable, et rend un second free inoffensifcontre le pointeur pendant
  • Pour éviter une fuite mémoire : sans cette ligne, le bloc reste allouécontre la fuite

Réponse : free rend la mémoire à l'allocateur, mais la VARIABLE p n'est pas touchée — elle contient toujours l'ancienne adresse. Deux fautes deviennent alors possibles : utiliser p, ce qui lit ou écrit dans un bloc qui peut avoir été réattribué à autre chose ; et refaire free(p), ce qui corrompt les structures internes de l'allocateur. Poser p = NULL neutralise les deux : déréférencer NULL plante IMMÉDIATEMENT, à l'endroit exact de la faute, ce qui est de loin le meilleur comportement possible ; et free(NULL) est explicitement défini par la norme comme ne faisant rien. Cela n'a en revanche aucun effet sur la fuite, qui vient d'un free OUBLIÉ, ni sur l'allocateur, qui a déjà tout ce qu'il lui faut.

Deux structures qui deviennent possibles

Le tableau dynamique. On alloue une capacité initiale, et quand elle est atteinte on double avec realloc. Doubler plutôt qu'ajouter une case est ce qui rend l'insertion amortie en O(1)O(1) : nn insertions coûtent au total O(n)O(n), puisque les recopies successives forment une série géométrique. C'est ainsi que sont faits les tableaux extensibles de tous les langages, et c'est le « O(1)O(1) amorti » du chapitre 5 d'Algorithmique 2.

La liste chaînée. La cellule du chapitre 5 d'Algorithmique 2 devient enfin implémentable :

typedef struct Cellule {    int valeur;    struct Cellule *suivant;     /* le chaînage : un pointeur */} Cellule; Cellule *n = malloc(sizeof(*n));n->valeur = 12;n->suivant = tete;               /* d'abord raccrocher la suite */tete = n;                        /* puis déplacer la tête */

Les deux dernières lignes sont exactement celles du cours d'algorithmique, dans le même ordre — et l'on voit maintenant pourquoi il compte : tete = n d'abord rendrait l'ancienne liste inatteignable, donc définitivement fuite, puisque plus aucun pointeur ne la désignerait.

La flèche -> est une commodité : n->valeur s'écrirait sinon (*n).valeur, avec des parenthèses obligatoires car . est plus prioritaire que *.

Quiz · 1 question

Que reproche-t-on à l'écriture T = realloc(T, nouvelleTaille) ?

  • Rien : c'est l'idiome recommandé, realloc gérant elle-même l'ancien blocrien à redire
  • Si realloc échoue, elle rend NULL sans libérer l'ancien bloc — mais l'affectation vient d'écraser T : le seul pointeur vers l'ancien bloc est perdu, donc fuite garantie ET données perduesperte du pointeur en cas d'échec
  • realloc ne peut pas agrandir un bloc, seulement le rétrécirlimite de realloc

Réponse : En cas de succès, l'écriture est correcte. En cas d'ÉCHEC, realloc rend NULL et laisse l'ancien bloc intact et alloué — comportement délibéré, pour que l'appelant puisse se rabattre dessus. Mais l'affectation T = realloc(...) a déjà écrasé T avec NULL : plus personne ne connaît l'adresse de l'ancien bloc. On a donc perdu les données ET fui la mémoire, en une ligne. L'écriture correcte passe par une temporaire : void *tmp = realloc(T, n); if (tmp != NULL) T = tmp; else /* traiter l'échec, T reste valide */. À retenir aussi : en cas de succès, realloc peut avoir DÉPLACÉ le bloc, ce qui rend pendants tous les autres pointeurs qui visaient l'ancien emplacement.

À vous

L'exercice écrit un allocateur miniature — une zone découpée en blocs, avec malloc et free — puis l'instrumente pour détecter les quatre fautes. C'est un valgrind en trente lignes, et c'est ce qui rend les fautes visibles avant le chapitre 10.

Quatre temps. Allouer, écrire, libérer, et constater qu'un bilan de fin de programme signale les blocs jamais rendus. Provoquer un pointeur pendant, puis un double free, et voir l'allocateur les diagnostiquer. Écrire le tableau dynamique qui double sa capacité, et compter les recopies pour vérifier qu'elles sont bien en O(n)O(n) au total. Enfin, construire et libérer une liste chaînée — en gardant le pointeur suivant avant de libérer la cellule, sinon l'allocateur vous le dira.

Exercice de code

Écrivez un allocateur instrumenté, détectez les quatre fautes, puis mesurez le doublement de capacité.

Point de départ

// ── Un allocateur instrumenté ─────────────────────────────────────────────
function creerAllocateur() {
  const blocs = new Map();     // adresse -> { taille, vivant, contenu }
  const fautes = [];
  let prochaine = 0x2000;

  return {
    malloc(octets) {
      const adresse = prochaine;
      prochaine += octets + 16;                  // + marge, comme un vrai tas
      blocs.set(adresse, { taille: octets, vivant: true, contenu: new Array(octets).fill(null) });
      return adresse;
    },
    calloc(n, taille) {
      const a = this.malloc(n * taille);
      blocs.get(a).contenu.fill(0);
      return a;
    },
    ecrire(adresse, decalage, valeur) {
      const b = blocs.get(adresse);
      if (!b) { fautes.push("écriture à une adresse jamais allouée"); return; }
      if (!b.vivant) { fautes.push("ÉCRITURE APRÈS LIBÉRATION à 0x" + adresse.toString(16)); return; }
      // ← à écrire : refuser un décalage hors du bloc (débordement de tas)
      b.contenu[decalage] = valeur;
    },
    lire(adresse, decalage) {
      const b = blocs.get(adresse);
      if (!b) { fautes.push("lecture à une adresse jamais allouée"); return undefined; }
      if (!b.vivant) { fautes.push("LECTURE APRÈS LIBÉRATION à 0x" + adresse.toString(16)); return undefined; }
      return b.contenu[decalage];
    },
    free(adresse) {
      const b = blocs.get(adresse);
      if (!b) { fautes.push("free d'une adresse jamais allouée"); return; }
      // ← à écrire : détecter le double free
      b.vivant = false;
    },
    bilan() {
      let fuite = 0, n = 0;
      for (const [a, b] of blocs) if (b.vivant) { fuite += b.taille; n++; }
      console.log("   " + n + " bloc(s) jamais libéré(s), " + fuite + " octets perdus");
      for (const f of fautes) console.log("   FAUTE : " + f);
      if (n === 0 && fautes.length === 0) console.log("   aucune fuite, aucune faute");
    },
  };
}

// ── À VOUS ────────────────────────────────────────────────────────────────
// 1. Complétez la détection du débordement de bloc et du double free.
// 2. Écrivez un tableau dynamique qui DOUBLE sa capacité, et comptez les
//    recopies pour n insertions. Comparez à la stratégie « + 1 case ».
// 3. Construisez puis libérez une liste chaînée. Attention à l'ordre :
//    lire le champ « suivant » AVANT de libérer la cellule.

const A = creerAllocateur();
const t = A.malloc(4 * 5);
A.ecrire(t, 0, 42);
console.log("relu :", A.lire(t, 0));
A.free(t);
A.bilan();

Solution

function creerAllocateur() {
  const blocs = new Map();
  const fautes = [];
  let prochaine = 0x2000;
  let recopies = 0;

  return {
    malloc(octets) {
      const adresse = prochaine;
      prochaine += octets + 16;
      blocs.set(adresse, { taille: octets, vivant: true, contenu: new Array(octets).fill(null) });
      return adresse;
    },
    calloc(n, taille) { const a = this.malloc(n * taille); blocs.get(a).contenu.fill(0); return a; },
    realloc(adresse, octets) {
      const b = blocs.get(adresse);
      const neuf = this.malloc(octets);
      // realloc RECOPIE puis libère : c'est ce coût qu'on va compter.
      for (let i = 0; i < Math.min(b.taille, octets); i++) {
        blocs.get(neuf).contenu[i] = b.contenu[i];
        recopies++;
      }
      b.vivant = false;
      return neuf;
    },
    ecrire(adresse, decalage, valeur) {
      const b = blocs.get(adresse);
      if (!b) { fautes.push("écriture à une adresse jamais allouée"); return; }
      if (!b.vivant) { fautes.push("ÉCRITURE APRÈS LIBÉRATION à 0x" + adresse.toString(16)); return; }
      // Débordement de tas : au-delà du bloc, on écrase les métadonnées de
      // l'allocateur, et le plantage survient bien plus tard, ailleurs.
      if (decalage < 0 || decalage >= b.taille) {
        fautes.push("DÉBORDEMENT DE BLOC : décalage " + decalage + " dans un bloc de " + b.taille);
        return;
      }
      b.contenu[decalage] = valeur;
    },
    lire(adresse, decalage) {
      const b = blocs.get(adresse);
      if (!b) { fautes.push("lecture à une adresse jamais allouée"); return undefined; }
      if (!b.vivant) { fautes.push("LECTURE APRÈS LIBÉRATION à 0x" + adresse.toString(16)); return undefined; }
      return b.contenu[decalage];
    },
    free(adresse) {
      if (adresse === 0) return;                 // free(NULL) ne fait rien
      const b = blocs.get(adresse);
      if (!b) { fautes.push("free d'une adresse jamais allouée"); return; }
      if (!b.vivant) { fautes.push("DOUBLE FREE à 0x" + adresse.toString(16)); return; }
      b.vivant = false;
    },
    recopies: () => recopies,
    bilan(titre) {
      let fuite = 0, n = 0;
      for (const [, b] of blocs) if (b.vivant) { fuite += b.taille; n++; }
      console.log("   " + titre);
      console.log("      " + n + " bloc(s) jamais libéré(s), " + fuite + " octets perdus");
      for (const f of fautes) console.log("      FAUTE : " + f);
      if (n === 0 && fautes.length === 0) console.log("      aucune fuite, aucune faute");
    },
  };
}

console.log("— 1. les quatre fautes —");
const A = creerAllocateur();
const t = A.malloc(4 * 5);
A.ecrire(t, 0, 42);
A.ecrire(t, 19, 7);        // décalage hors du bloc de 20 octets ? non : 19 < 20, ok
A.ecrire(t, 25, 7);        // débordement
A.free(t);
A.lire(t, 0);              // lecture après libération
A.free(t);                 // double free
const fuite = A.malloc(64); // jamais libéré
A.bilan("bilan");

console.log("");
console.log("— 2. tableau dynamique : doubler contre ajouter une case —");
for (const [nom, croissance] of [["doubler", (c) => c * 2], ["+ 1 case", (c) => c + 1]]) {
  const B = creerAllocateur();
  let capacite = 1, taille = 0;
  let bloc = B.malloc(capacite * 4);
  for (let i = 0; i < 1000; i++) {
    if (taille === capacite) { capacite = croissance(capacite); bloc = B.realloc(bloc, capacite * 4); }
    B.ecrire(bloc, taille, i);
    taille++;
  }
  console.log("   " + nom.padEnd(10) + " : " + String(B.recopies()).padStart(6) +
              " recopies pour 1000 insertions" +
              (nom === "doubler" ? "   (O(n) au total, donc O(1) amorti)" : "   (O(n²))"));
}

console.log("");
console.log("— 3. construire et libérer une liste chaînée —");
const C = creerAllocateur();
const VALEUR = 0, SUIVANT = 1;
let tete = 0;
for (const v of [3, 2, 1]) {
  const n = C.malloc(2 * 8);
  C.ecrire(n, VALEUR, v);
  C.ecrire(n, SUIVANT, tete);   // d'abord raccrocher la suite
  tete = n;                      // puis déplacer la tête
}
const vus = [];
for (let c = tete; c !== 0; c = C.lire(c, SUIVANT)) vus.push(C.lire(c, VALEUR));
console.log("   liste : " + vus.join(" -> "));

// L'ordre est imposé : lire le champ suivant AVANT de libérer la cellule,
// sinon on lit dans un bloc déjà rendu.
let c = tete;
while (c !== 0) {
  const suivant = C.lire(c, SUIVANT);
  C.free(c);
  c = suivant;
}
C.bilan("après libération");

En travaux pratiques

Travaux pratiques 8 · 4 h

Gérer la mémoire soi-même

Allouer ce dont on ne connaît pas la taille à l'avance, mesurer une fuite, et rencontrer les trois fautes d'allocation que valgrind détecte.

Avant de commencer

  • Le TP 7 : pointeurs et outils
  • valgrind installé

Énoncé

  1. Le tableau de taille inconnueÉcrivez une fonction qui lit tous les entiers d'un fichier dans un tableau alloué dynamiquement, en doublant la capacité quand elle est pleine. Testez sur 0, 1 et un million de valeurs.
  2. L'allocation qui échoueDemandez délibérément une allocation énorme et vérifiez le retour. Puis retirez le test et observez ce que fait le programme.
  3. Mesurer une fuiteÉcrivez une boucle qui alloue sans libérer et surveillez la mémoire du processus. Passez ensuite valgrind et lisez le résumé.
  4. Les trois fautesProvoquez successivement une double libération, une utilisation après libération, et une libération d'un pointeur non alloué. Notez ce que dit valgrind pour chacune.
  5. realloc, et son piègeUtilisez realloc en réaffectant le résultat au pointeur d'origine, puis provoquez un échec d'allocation. Expliquez la fuite. Écrivez ensuite la forme correcte. Indice : Si realloc échoue, il renvoie NULL — et l'ancien bloc, lui, existe toujours.
  6. Qui libèreÉcrivez une fonction qui renvoie une chaîne allouée. Documentez le contrat, puis écrivez la fonction de libération qui va avec, et utilisez-la partout.
  7. Au fil rougeFaites lire à journal un fichier de taille quelconque, en allouant ce qu'il faut. Le programme doit se terminer avec zéro octet perdu selon valgrind.
  8. Mesurer le coûtComparez un million de petites allocations à une seule grande découpée à la main. Chronométrez les deux.

C'est réussi quand

  • Votre lecteur gère un million de valeurs sans connaître la taille à l'avance
  • valgrind annonce « All heap blocks were freed » sur votre fil rouge
  • Vous savez écrire la forme correcte de realloc sans la relire

Correction

Le tableau qui granditlecture.c
int *valeurs = NULL;
size_t n = 0, capacite = 0;

while (fscanf(f, "%d", &v) == 1) {
  if (n == capacite) {
      size_t nouvelle = capacite ? capacite * 2 : 16;
      int *tmp = realloc(valeurs, nouvelle * sizeof *valeurs);
      if (!tmp) { free(valeurs); return -1; }    /* forme CORRECTE */
      valeurs = tmp;
      capacite = nouvelle;
  }
  valeurs[n++] = v;
}

Le doublement donne un coût amorti constant par insertion : n insertions coûtent au total O(n) copies, pas O(n²). C'est exactement le mécanisme des tableaux dynamiques de tous les langages, et vous le retrouverez démontré en Algorithmique 2. Notez sizeof *valeurs plutôt que sizeof(int) : si le type change, la ligne reste juste.

Le retour qu'on oublie de tester
int *p = malloc(100000000000UL);
if (!p) { fprintf(stderr, "mémoire insuffisante\n"); return 1; }

sans le test :
p vaut NULL, puis *p = 0 → Segmentation fault
mais AILLEURS que là où l'allocation a échoué

Tester le retour ne sauve pas le programme, il rend l'échec LISIBLE : un message clair au bon endroit plutôt qu'un plantage cinquante lignes plus loin. Sur un système Linux avec surréservation (TP 6 de Systèmes), malloc réussit d'ailleurs souvent quand même — l'échec surviendra au premier accès, et pas sous une forme que votre test puisse voir.

Le rapport de valgrind
valgrind --leak-check=full ./journal acces.log

==4412== HEAP SUMMARY:
==4412==   in use at exit: 40,960 bytes in 3 blocks
==4412==   total heap usage: 1,204 allocs, 1,201 frees
==4412== 
==4412== 40,960 bytes in 3 blocks are definitely lost
==4412==    at malloc (vg_replace_malloc.c:381)
==4412==    by lire_lignes (lecture.c:24)
==4412==    by main (main.c:12)

/* objectif : */
==4412== All heap blocks were freed -- no leaks are possible

valgrind donne la ligne de l'ALLOCATION perdue, pas celle où le manque se voit — c'est ce qui le rend utilisable. « definitely lost » signifie qu'aucun pointeur ne mène plus au bloc ; « still reachable » signifie qu'il n'a pas été libéré mais reste atteignable, ce qui est bénin en fin de programme et grave dans une boucle.

Les trois fautes
free(p); free(p);        → Invalid free() / delete / delete[]
                          (le tas est corrompu : peut être EXPLOITABLE)

free(p); *p = 1;         → Invalid write of size 4
                          Address is 0 bytes inside a block of size 40 free'd

int t[10]; free(t);      → Invalid free(): pas une adresse du tas

/* la règle qui neutralise les deux premières */
free(p); p = NULL;       /* free(NULL) est autorisé et ne fait rien */

Remettre à NULL après libération transforme une double libération en opération inoffensive et une utilisation après libération en plantage net à l'endroit exact. Deux caractères qui convertissent des bogues exploitables en erreurs franches : le meilleur rapport de tout le cours.

Le piège de realloc
/* FAUX : si realloc échoue, l'ancien bloc est PERDU */
p = realloc(p, n);
if (!p) return -1;          /* fuite : l'ancien p n'existe plus nulle part */

/* CORRECT */
void *tmp = realloc(p, n);
if (!tmp) { free(p); return -1; }
p = tmp;

realloc peut renvoyer une adresse DIFFÉRENTE — le bloc a peut-être été déplacé —, et tous les pointeurs qui visaient l'intérieur de l'ancien bloc deviennent pendants. Deux conséquences : passer par une variable temporaire, et ne jamais garder de pointeur vers l'intérieur d'un tableau susceptible d'être réalloué. C'est le même piège que l'invalidation des itérateurs dans les langages à collections.

Le coût de l'allocation
1 000 000 malloc(16) + free     : 0,094 s
1 malloc(16 Mo) découpé à la main : 0,004 s

malloc doit chercher un bloc libre, découper, mettre à jour
les métadonnées, et peut demander de la mémoire au noyau

Un facteur 20, sans changer un seul calcul. C'est pourquoi les programmes qui allouent beaucoup de petits objets de même taille utilisent une réserve — un grand bloc découpé à l'avance. Le principe est le même qu'au TP 8 d'Architecture et qu'au tampon de printf : regrouper les opérations coûteuses au lieu de les répéter.

Ce que la suite en fait

Le bloc V donne aux données une forme et une persistance : la structure struct regroupe des champs de types différents — et l'on vient déjà d'en écrire une, la cellule de liste — puis les fichiers les font survivre à la fin du programme.

Le chapitre 10 fournit enfin l'outillage. valgrind détecte exactement les quatre fautes de ce chapitre, sur du vrai code, sans instrumentation à écrire soi-même — et il donne la ligne de l'allocation fautive, ce qui est la seule information réellement utile quand la faute et le symptôme sont à mille lignes l'un de l'autre.

À retenir

Flashcards · 5 cartes

Qu'est-ce qui distingue la pile du tas, et pourquoi le tas est-il nécessaire ?
La PILE est gérée automatiquement : allouée à l'entrée d'un bloc, libérée à sa sortie, très rapide, mais de taille connue à la compilation et de durée de vie limitée au bloc. Le TAS est géré à la main : vous allouez, vous libérez, avec une taille décidée à l'exécution et une donnée qui SURVIT à la fonction qui l'a créée. C'est ce dernier point qui le rend nécessaire — une fonction ne peut pas rendre l'adresse d'une de ses locales, dont le cadre est détruit au retour.
Quelles précautions entourent malloc et realloc ?
malloc prend un nombre d'OCTETS et rend un contenu INDÉFINI — on écrit n * sizeof(*T), qui reste correct si le type change, et calloc met à zéro. Elle peut ÉCHOUER et rendre NULL : le tester transforme une pénurie gérable en erreur de segmentation si on l'omet. realloc peut DÉPLACER le bloc (tous les autres pointeurs deviennent pendants) et, en cas d'échec, rend NULL sans libérer l'ancien : écrire T = realloc(T, n) perd alors le seul pointeur vers les données. On passe par une temporaire.
Quelles sont les trois règles de discipline sur la propriété d'un bloc ?
Le C n'a pas de ramasse-miettes : la propriété est une CONVENTION portée par la documentation. 1) Un malloc, un free, si possible dans la même fonction ou dans la fonction jumelle (creerPile / detruirePile). 2) Libérer dans l'ordre inverse de la construction, en gardant le pointeur suivant AVANT de libérer une cellule. 3) Poser p = NULL après free.
Nommez les quatre fautes d'allocation et leur point commun.
La FUITE : un bloc que plus aucun pointeur ne désigne — anodin sur un utilitaire, fatal sur un service qui tourne des mois. Le POINTEUR PENDANT : utiliser un bloc après free. Le DOUBLE FREE : corrompt les structures de l'allocateur. Le DÉBORDEMENT DE TAS : écrire au-delà du bloc, ce qui écrase ses métadonnées. Point commun : la faute et le symptôme sont ÉLOIGNÉS — d'où valgrind.
Pourquoi un tableau dynamique double-t-il sa capacité au lieu d'ajouter une case ?
Parce que doubler rend l'insertion amortie en O(1) : les recopies successives forment une série géométrique dont la somme est O(n) pour n insertions. Ajouter une case à chaque fois donnerait une recopie par insertion, soit O(n²) au total. C'est ainsi que sont faits les tableaux extensibles de tous les langages, et c'est le « O(1) amorti » d'Algorithmique 2.