Menu

Mémoire dynamique en C++ : new et delete expliqués

Comment allouer de la mémoire à l'exécution avec new, la libérer avec delete, et éviter les fuites, les pointeurs ballants et les doubles libérations qui accompagnent la gestion manuelle du tas.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

Pourquoi demander de la mémoire à l'exécution

Jusqu'à présent, chaque variable que vous avez créée a vécu sur la pile (stack) : sa taille est connue à la compilation et elle est détruite automatiquement à la fin de sa portée. C'est rapide et sûr, mais cela ne convient pas au cas où vous ignorez la quantité de mémoire nécessaire avant l'exécution du programme : un tampon dimensionné par une saisie utilisateur, une structure qui doit survivre à la fonction qui l'a créée, ou un graphe dont la forme n'est pas connue à l'avance.

Pour ces cas, C++ vous permet d'allouer depuis le tas (aussi appelé free store) avec new, et de le restituer avec delete. L'expression new réserve un bloc, exécute le constructeur et renvoie un pointeur vers celui-ci, en s'appuyant directement sur les pointeurs vus à la page précédente.

La variable p elle-même vit sur la pile : ce n'est qu'un pointeur. L'int vers lequel elle pointe vit sur le tas et reste en vie jusqu'à ce que vous le libériez avec delete, peu importe combien de portées se succèdent.

Pile face au tas

Cette distinction est la raison même de l'existence de new, il vaut donc la peine de la rendre concrète.

void demo() {
    int a = 10;            // sur la pile - disparaît quand demo() retourne
    int* b = new int(10);  // 'b' sur la pile, l'int pointé sur le tas
}                          // 'a' détruit ; l'int du tas FUIT - jamais libéré

Différences essentielles :

  • Pile - durée de vie automatique, très rapide, taille limitée (généralement quelques Mo), libérée pour vous à la sortie de la portée.
  • Tas - durée de vie manuelle, légèrement plus lente, grand, et libéré uniquement lorsque vous appelez delete.

Le compromis échange la flexibilité contre la responsabilité : la mémoire du tas vit exactement aussi longtemps que vous le voulez, mais c'est à vous qu'il revient de penser à la libérer.

Allouer des tableaux avec new[]

Lorsque vous avez besoin d'un bloc dont la longueur est décidée à l'exécution, utilisez la forme tableau new T[n]. Elle renvoie un pointeur vers le premier élément, et vous le libérez avec le delete[] correspondant.

La règle est stricte et facile à enfreindre : la mémoire issue de new se libère avec delete, et celle issue de new[] se libère avec delete[]. Les mélanger (delete arr sur quelque chose alloué avec new[]) relève du comportement indéfini, même si cela semble fonctionner sur votre machine.

Les trois bogues classiques

La gestion manuelle de la mémoire comporte un petit ensemble d'erreurs qui représentent la plupart des bogues du tas. Apprenez à reconnaître les trois.

1. Fuite de mémoire - vous n'appelez jamais delete. Le bloc reste réservé pour toujours. Inoffensif une fois, fatal dans une boucle.

void leaky() {
    int* p = new int(5);
    // ... pas de delete ...
}   // p disparaît ; l'int du tas est désormais inaccessible ET non libéré

2. Pointeur ballant - vous utilisez la mémoire après l'avoir libérée. Le pointeur conserve l'ancienne adresse, mais cette mémoire ne vous appartient plus.

3. Double libération - vous faites delete deux fois sur le même bloc. Cela corrompt la comptabilité interne du tas et provoque généralement un plantage.

int* p = new int(1);
delete p;
delete p;   // double libération - comportement indéfini, souvent un plantage

Mettre un pointeur à nullptr après l'avoir libéré désamorce à la fois l'usage ballant et la double libération : déréférencer nullptr plante immédiatement (facile à déboguer), et delete nullptr est explicitement une opération sûre qui ne fait rien.

Un cycle réaliste allouer-utiliser-libérer

En assemblant le tout, voici la forme d'une gestion manuelle correcte : allouer, utiliser, libérer exactement une fois, et ne plus toucher au pointeur ensuite.

Remarquez que delete u fait deux choses pour un type classe : il exécute d'abord le destructeur de l'objet, puis libère la mémoire brute. Cet ordre compte dès que vos objets possèdent leurs propres ressources.

Un piège subtil : si une exception est levée entre new et delete, le delete ne s'exécute jamais et vous provoquez une fuite. Envelopper chaque allocation dans un try/catch pour gérer cela est fastidieux et source d'erreurs, ce qui est précisément le problème que résout la page suivante.

Suivant : Les pointeurs intelligents

Vous avez désormais vu le coût total de la gestion manuelle de la mémoire : chaque new est une promesse de delete ultérieur, et une seule libération oubliée, doublée ou prématurée est un comportement indéfini. Le C++ moderne ne fait presque jamais cette promesse à la main. La page suivante présente les pointeurs intelligents - std::unique_ptr et std::shared_ptr - des objets qui possèdent une allocation du tas et appellent delete pour vous automatiquement lorsqu'ils sortent de portée, transformant les trois bogues classiques en problèmes que le compilateur et le RAII gèrent à votre place.

Questions fréquentes

Quelle est la différence entre new et delete en C++ ?

new alloue de la mémoire sur le tas à l'exécution et renvoie un pointeur vers celle-ci ; delete libère la mémoire qui a été allouée avec new. Chaque new doit être associé à exactement un delete, sinon vous provoquez une fuite de mémoire. Pour les tableaux, utilisez new[] avec delete[].

Que se passe-t-il si vous oubliez d'appeler delete en C++ ?

Vous obtenez une fuite de mémoire : le bloc du tas reste réservé pendant toute la durée de vie de votre programme, même si plus rien ne pointe vers lui. Une seule fuite est généralement sans danger, mais les fuites dans une boucle ou dans un service de longue durée s'accumulent jusqu'à ce que le programme manque de mémoire et plante.

Devrais-je utiliser new et delete directement en C++ moderne ?

Rarement. Préférez des conteneurs comme std::vector ou des pointeurs intelligents (std::unique_ptr, std::shared_ptr) qui libèrent la mémoire automatiquement. Il vaut la peine de comprendre new/delete bruts car les pointeurs intelligents les encapsulent, mais dans le code de tous les jours, c'est une source de fuites et de pointeurs ballants.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER