Programmation • IoT

Chapitre 1 – L’allocation dynamique de la mémoire

Document réservé

Vous consultez actuellement la présentation publique de ce chapitre.

Les documents PDF complets, comprenant les développements théoriques, les exemples détaillés et les exercices, sont disponibles sur demande.

Pour obtenir un accès, contactez-moi via la page Contact en indiquant les domaines qui vous intéressent (C++, ESP-IDF, électronique, etc.).

1 L’allocation dynamique de mémoire 1.1 Toute variable occupe de la mémoire . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.1.1 Observons la disposition des variables en mémoire . . . . . . . . . . . . . . . . 1.1.2 Des variables de tailles différentes . . . . . . . . . . . . . . . . . . . . . . . . . 1.1.3 L’ordre des types peut avoir une influence . . . . . . . . . . . . . . . . . . . . 1.1.4 Un pointeur est lui-même une variable . . . . . . . . . . . . . . . . . . . . . . 1.1.5 Quelle taille occupe un pointeur ? . . . . . . . . . . . . . . . . . . . . . . . . . 1.2 La durée de vie automatique et la pile . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.3 Lorsque plusieurs appels existent simultanément . . . . . . . . . . . . . . . . . . . . . 1.3.2 Une durée de vie automatique . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.3.3 La pile n’est pas infinie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.4 Pourquoi l’allocation automatique ne suffit-elle pas toujours ? . . . . . . . . . . . . . 1.4.1 Retour au problème de la matrice . . . . . . . . . . . . . . . . . . . . . . . . . 1.4.2 Une autre limite : la durée de vie . . . . . . . . . . . . . . . . . . . . . . . . . . 1.6 Première allocation dynamique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.6.1 Demander de la mémoire avec new . . . . . . . . . . . . . . . . . . . . . . . . 1.6.2 Conserver l’adresse obtenue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.6.3 Observons les deux objets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.7 Libérer la mémoire avec delete . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.8 Que devient le pointeur après delete ? . . . . . . . . . . . . . . . . . . . . . . . . . . 1.8.2 Que se passe-t-il si nous utilisons encore *p ? . . . . . . . . . . . . . . . . . . . 1.8.3 Le pointeur nul : nullptr . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.8.4 Déréférencer un pointeur nul . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.10 Allocation dynamique d’un tableau . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.10.1 Des objets contigus en mémoire . . . . . . . . . . . . . . . . . . . . . . . . . . 1.10.2 Une allocation beaucoup plus importante . . . . . . . . . . . . . . . . . . . . . 1.10.3 Utiliser réellement la mémoire . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.10.4 Libérer un tableau dynamique . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.10.5 La mémoire libérée ne disparaît pas nécessairement immédiatement . . . . . . 1.10.6 Réutilisation de la zone libérée . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.10.7 Pile et tas : deux gestions différentes de la mémoire . . . . . . . . . . . . . . . 1.11 Que se passe-t-il si une allocation échoue ? . . . . . . . . . . . . . . . . . . . . . . . . 1.11.1 Le comportement de new . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.12.1 Perdre l’adresse d’une allocation . . . . . . . . . . . . . . . . . . . . . . . . . . 1.12.2 Pourquoi est-ce un problème ? . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.12.3 Une fuite peut également être provoquée par une affectation . . . . . . . . . 1.12.4 La fuite peut rester invisible . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2026 – C++ Partie III L’allocation dynamique de mémoire
Accueil
L’allocation dynamique de mémoire Toute variable occupe de la mémoire Lorsqu’une variable est créée dans un programme, une certaine quantité de mémoire doit être réservée pour stocker sa valeur. Considérons par exemple : Ces trois variables n’occupent pas nécessairement la même quantité de mémoire. L’opérateur sizeof, déjà rencontré précédemment, permet de connaître la taille occupée par un type ou par une variable. std::cout << sizeof(a) << ’\n’; std::cout << sizeof(x) << ’\n’; std::cout << sizeof(lettre) << ’\n’; Sur une machine courante, on pourra par exemple obtenir : Cela signifie que, sur cette machine : 2026 – C++ Partie III L’allocation dynamique de mémoire double −→ 8 octets La taille de certains types dépend de la machine et du compilateur utilisés. Il ne faut donc pas considérer, par exemple, qu’un int occupe obligatoirement 4 octets sur toutes les architectures. L’opérateur sizeof permet précisément de demander cette information au compilateur. Mais connaître la quantité de mémoire occupée ne suffit pas. Cette mémoire se trouve quelque part. Nous savons déjà qu’il est possible d’obtenir l’adresse d’une variable grâce à l’opérateur &. 9 std::cout << « Adresse de a :  » << &a << ’\n’; 10 std::cout << « Adresse de x :  » << &x << ’\n’; L’exécution pourra produire, par exemple : Adresse de a : 0x16f3a2b4c 2 Adresse de x : 0x16f3a2b40 Ces valeurs ne doivent pas être apprises ni interprétées pour l’instant. Elles indiquent simplement que les variables a et x sont stockées à deux endroits déterminés de la mémoire accessible au programme. Une variable possède donc au moins deux caractéristiques importantes du point de vue • une taille, que l’on peut connaître avec sizeof ; • une adresse, que l’on peut obtenir avec l’opérateur &. Nous allons maintenant utiliser ces deux informations comme instruments d’observation. L’objectif de cette partie ne sera pas seulement d’apprendre à demander de la mémoire au programme. Nous allons essayer de voir ce qui se passe réellement lorsque de la mémoire est réservée, utilisée puis
Accueil
2026 – C++ Partie III L’allocation dynamique de mémoire Observons la disposition des variables en mémoire Nous savons maintenant qu’une variable possède une taille et une adresse. Essayons de voir comment plusieurs variables sont disposées en mémoire. Considérons d’abord trois variables du même type : std::cout << « Taille d’un int :  » << sizeof(int) <<  » octets\n\n »; std::cout << « Adresse de a :  » << &a << ’\n’; std::cout << « Adresse de b :  » << &b << ’\n’; std::cout << « Adresse de c :  » << &c << ’\n’; Sur la machine utilisée pour cette expérience, nous obtenons : Taille d’un int : 4 octets Adresse de a : 0x16fdfee78 Adresse de b : 0x16fdfee74 5 Adresse de c : 0x16fdfee70 Les adresses diffèrent ici de 4 octets : Dans cette exécution, les trois variables ont donc été placées les unes à la suite des autres, chacune Les adresses sont écrites en hexadécimal. Il faut donc se rappeler que : Des variables de tailles différentes Modifions maintenant l’expérience :
Accueil
2026 – C++ Partie III L’allocation dynamique de mémoire std::cout << sizeof(a) << ’\n’; std::cout << sizeof(b) << ’\n’; std::cout << sizeof(c) << ’\n’; std::cout << &a << ’\n’; std::cout << &b << ’\n’; std::cout << &c << ’\n’; Nous avons mesuré : Adresse de a : 0x16fdfee70 Adresse de b : 0x16fdfee6c Adresse de c : 0x16fdfee60 Cette fois, la différence entre les adresses de b et de c vaut : Pourtant, c n’occupe que 8 octets. Nous observons donc un espace de 4 octets qui n’appartient ni à b ni à c. Ce phénomène est lié à une contrainte importante imposée par la machine : l’alignement des données en mémoire. Certains types sont de préférence placés à des adresses respectant certaines frontières. Sur la machine utilisée ici, les objets de type double sont notamment alignés sur des adresses multiples de 8 octets. Le compilateur peut donc laisser certains octets inutilisés afin de respecter ces contraintes. L’ordre des types peut avoir une influence Essayons maintenant : Nous obtenons cette fois : Adresse de a : 0x16fdfee70 Adresse de b : 0x16fdfee68 Adresse de c : 0x16fdfee64 Les écarts sont exactement de 8 puis de 4 octets. Dans cette expérience, aucun espace supplémentaire n’apparaît donc entre les trois variables.
Accueil
2026 – C++ Partie III L’allocation dynamique de mémoire • La valeur contenue dans une variable ne modifie pas la taille occupée par son type. • En revanche, la disposition en mémoire de données de types différents doit respecter certaines contraintes d’alignement. • Des espaces inutilisés peuvent donc apparaître entre certaines données. Les expériences précédentes montrent ce que notre compilateur a choiside faire lors de ces Le langage C++ ne garantit pas que des variables locales indépendantes seront placées les unes à la suite des autres en mémoire, ni qu’elles apparaîtront dans l’ordre de leur déclaration. Il ne faut donc pas déduire une règle générale du C++ à partir des adresses que nous venons Nous retrouverons plus tard la question de l’alignement lorsque nous étudierons la disposition des membres d’une structure ou d’une classe. Un pointeur est lui-même une variable std::cout << « Valeur de a :  » << a << ’\n’; std::cout << « Adresse de a :  » << &a << « \n\n »; std::cout << « Valeur de p :  » << p << ’\n’; std::cout << « Adresse de p :  » << &p << ’\n’; Sur notre machine, nous avons par exemple obtenu : Adresse de a : 0x16fdfee78 Valeur de p : 0x16fdfee78 Adresse de p : 0x16fdfee70 Nous constatons que : Le pointeur p contient donc l’adresse de a. Mais p est lui-même une variable donc il occupe de la mémoire et possède sa propre adresse, obtenue avec &p.
Accueil
2026 – C++ Partie III L’allocation dynamique de mémoire Il ne faut pas confondre : • p : la valeur contenue dans le pointeur, c’est-à-dire l’adresse de l’objet pointé ; • &p : l’adresse du pointeur lui-même. Quelle taille occupe un pointeur ? Mesurons maintenant la taille de plusieurs pointeurs : std::cout << sizeof(p) << ’\n’; std::cout << sizeof(q) << ’\n’; std::cout << sizeof(r) << ’\n’; Sur notre machine, nous obtenons : La taille du pointeur ne dépend donc pas ici de la taille de l’objet qu’il peut désigner. Un pointeur vers un char de 1 octet occupe lui-même 8 octets, tout comme un pointeur vers un double de 8 Cela s’explique simplement : le pointeur ne contient pas l’objet mais il contient une adresse permettant de retrouver cet objet. Un pointeur possède donc lui-même un coût en mémoire. Ce coût peut être important si un programme utilise un très grand nombre de pointeurs pour désigner de petits objets. Il devient en revanche négligeable lorsqu’un pointeur permet d’accéder à une grande quantité de données. Comme toujours, lorsqu’une consommation de mémoire devient importante, il est préférable de la mesurer plutôt que de la supposer. La durée de vie automatique et la pile Dans les expériences précédentes, nous avons observé où certaines variables étaient placées en mémoire. Une autre question est maintenant essentielle : Pendant combien de temps cette mémoire reste-t-elle utilisée ? Considérons la fonction suivante :
Accueil
2026 – C++ Partie III L’allocation dynamique de mémoire std::cout << « Adresse de a :  » << &a << ’\n’; Sur notre machine, les trois appels successifs ont affiché la même adresse pour a. Lors du premier appel de fonction(), la variable locale a est créée. Lorsque l’exécution quitte la fonction, cette variable cesse d’exister et la mémoire qu’elle utilisait peut alors être réutilisée. Lors du deuxième appel, une nouvelle variable a est créée. Dans notre expérience, le programme a réutilisé le même emplacement mémoire. Il en a été de même lors du troisième appel. Le langage C++ ne garantit pas que la même adresse sera obtenue lors de chaque appel. C’est le comportement que nous avons observé avec notre programme et notre compilateur. Il illustre cependant un principe fondamental : la mémoire occupée par une variable locale peut être réutilisée lorsque la durée de vie de cette variable est terminée. Lorsque plusieurs appels existent simultanément Modifions maintenant notre expérience en utilisant une fonction récursive : void fonction(int n) std::cout << « n =  » << n adresse de a :  » << &a << ’\n’;
Accueil
2026 – C++ Partie III L’allocation dynamique de mémoire Cette fois, nous avons obtenu : adresse de a : 0x16fdfee58 adresse de a : 0x16fdfee38 adresse de a : 0x16fdfee18 adresse de a : 0x16fdfedf8 La situation est différente de celle de l’expérience précédente. Lorsque fonction(3) appelle fonction(2), le premier appel n’est pas terminé. Sa variable locale a doit donc continuer à exister. Il en est de même lorsque fonction(2) appelle fonction(1), puis lorsque fonction(1) appelle fonction(0). Plusieurs appels de la même fonction existent donc simultanément, et chacun possède sa propre Pour permettre à une fonction d’en appeler une autre puis de reprendre son exécution lorsque cet appel est terminé, le programme doit conserver certaines informations qui sont organisées dans une zone de mémoire généralement appelée la pile, ou stack. Lorsqu’une fonction est appelée, un nouvel espace est utilisé sur cette pile pour conserver les informations nécessaires à cet appel. On parle généralement d’un cadre de pile, ou stack frame. Ce cadre peut notamment contenir des variables locales ainsi que des informations nécessaires au fonctionnement et au retour de la fonction. Lorsque la fonction se termine, son cadre de pile n’est plus nécessaire donc l’espace correspondant peut être réutilisé lors d’appels ultérieurs. Dans notre expérience récursive, nous avons mesuré un écart de : entre les adresses successives de a. sizeof(a) = 4 octets. Ces 32 octets ne représentent donc pas simplement la taille de a. Ils reflètent la disposition des différents cadres de pile produite par cette compilation. La taille exacte d’un cadre de pile n’est pas déterminée simplement par la somme des tailles des variables locales. Elle dépend notamment de la fonction, du compilateur, de l’architecture, des contraintes d’alignement et des optimisations utilisées. L’écart de 32 octets mesuré dans notre expérience ne constitue donc pas une règle générale. Une durée de vie automatique Une variable locale ordinaire possède généralement une durée de vie automatique. Sa création et sa destruction sont liées à l’exécution du bloc auquel elle appartient.
Accueil
2026 – C++ Partie III L’allocation dynamique de mémoire // a n’existe plus ici Le programmeur n’a aucune opération particulière à effectuer pour libérer la mémoire associée à a car sa durée de vie est gérée automatiquement. • Les variables locales ordinaires ont une durée de vie automatique. • Lorsqu’une fonction est appelée, les informations nécessaires à son exécution sont généralement placées dans un cadre de pile. • Lorsque l’appel se termine, ce cadre n’est plus nécessaire et l’espace correspondant peut • Des appels de fonctions imbriqués nécessitent plusieurs cadres de pile simultanément. La pile n’est pas infinie La quantité de mémoire disponible pour la pile est limitée. Dans une fonction récursive, chaque nouvel appel nécessite généralement un nouveau cadre de pile. Si le nombre d’appels imbriqués devient trop important, la pile peut finir par manquer d’espace. Ce phénomène est appelé un dépassement de pile, ou stack overflow. Nous avions déjà rencontré cette difficulté lors de l’étude de la récursivité. Nous pouvons maintenant mieux comprendre son origine : chaque appel supplémentaire doit conserver son propre contexte tant que les appels précédents ne sont pas terminés. Sur un système disposant de ressources limitées, comme un microcontrôleur, la quantité de mémoire disponible impose donc une attention particulière à la profondeur des appels récursifs. Si la durée de vie automatique fonctionne si bien, pourquoi avons-nous besoin de l’allocation Pourquoi l’allocation automatique ne suffit-elle pas toujours ? La gestion automatique de la mémoire que nous venons d’observer est particulièrement pratique. Lorsque nous écrivons : le programme sait quelle quantité de mémoire est nécessaire :
Accueil
2026 – C++ Partie III L’allocation dynamique de mémoire De même, si nous écrivons : double valeurs[100]; le nombre d’éléments est connu et le programme peut prévoir l’espace nécessaire pour le tableau. Supposons maintenant que le nombre de valeurs à stocker ne soit connu qu’au moment de l’exécution std::cout << « Combien de valeurs ? « ; Lorsque le programme est écrit et compilé, nous ne connaissons pas encore la valeur de n. Elle pourrait être : La quantité de mémoire nécessaire dépend donc d’une information qui n’existera qu’au moment de Retour au problème de la matrice C’est exactement la difficulté que nous avions rencontrée avec notre classe Matrice à la partie II du Si nous voulons pouvoir écrire : Matrice B(100, 200); La première matrice doit pouvoir contenir : coefficients, tandis que la seconde doit pouvoir en contenir : Si les coefficients sont de type double, les quantités de mémoire nécessaires sont donc très différentes. Pourtant A et B sont deux objets du même type : Nous avons donc besoin d’un mécanisme permettant de demander une certaine quantité de mémoire pendant l’exécution du programme.
Accueil
2026 – C++ Partie III L’allocation dynamique de mémoire • La quantité de mémoire nécessaire à un programme n’est pas toujours connue lors de son écriture ou de sa compilation. • Certaines informations, comme le nombre d’éléments à stocker, peuvent n’être connues qu’au moment de l’exécution. Nous avons alors besoin de pouvoir réserver de la mémoire Une autre limite : la durée de vie La quantité de mémoire nécessaire n’est pas la seule difficulté. Nous avons vu qu’une variable locale ordinaire possède une durée de vie automatique. std::cout << a << ’\n’; La variable a est créée pendant l’exécution de la fonction et lorsque la fonction se termine, a cesse d’exister et la mémoire qu’elle utilisait peut être réutilisée. Cette gestion automatique est très pratique dans la plupart des cas. Mais supposons que nous voulions créer une donnée dans une fonction et continuer à l’utiliser après la fin de cette fonction. Nous nous heurtons alors à une difficulté : la durée de vie de la donnée ne doit plus nécessairement coïncider avec la durée de vie de la fonction qui l’a créée. Nous avons donc besoin d’un mécanisme permettant de créer une donnée dont la durée de vie pourra être contrôlée indépendamment du bloc dans lequel sa création a été demandée. L’allocation dynamique répond notamment à deux besoins : • réserver une quantité de mémoire déterminée au moment de l’exécution ; • permettre à certaines données d’avoir une durée de vie qui n’est pas directement imposée par le bloc dans lequel elles ont été créées. Nous avons observé que les variables locales ordinaires sont généralement gérées à l’aide de la pile. Cette organisation est particulièrement efficace lorsque la durée de vie des données suit naturellement celle des fonctions et des blocs qui les utilisent. Mais nous venons de rencontrer deux situations différentes : • la quantité de mémoire nécessaire n’est connue qu’au moment de l’exécution ;
Accueil
2026 – C++ Partie III L’allocation dynamique de mémoire • la durée de vie souhaitée d’une donnée ne correspond pas nécessairement à celle du bloc dans lequel elle est créée. Pour répondre à ces besoins, un programme peut utiliser une autre zone de son espace mémoire : le tas, appelé heap en anglais. Contrairement à la mémoire utilisée automatiquement pour les variables locales, la mémoire demandée dans le tas doit être explicitement obtenue pendant l’exécution du Cette mémoire reste réservée jusqu’à ce qu’elle soit explicitement libérée. Nous pouvons représenter très schématiquement la situation ainsi : espace mémoire du programme Cette représentation est volontairement simplifiée. Elle ne décrit pas précisément l’organisation réelle de la mémoire virtuelle d’un système moderne. Elle permet simplement de distinguer deux mécanismes de gestion de mémoire que nous allons maintenant Le tas n’est pas une immense variable ni un tableau que le programme pourrait parcourir Lorsqu’un programme a besoin de mémoire dynamique, il demande au système de gestion de mémoire de lui fournir une zone suffisamment grande. Le programme reçoit alors un moyen de retrouver cette zone. C’est ici que les pointeurs vont devenir indispensables. Première allocation dynamique Nous disposons maintenant des éléments nécessaires pour effectuer notre première allocation dynamique. Considérons l’instruction suivante :
Accueil
2026 – C++ Partie III L’allocation dynamique de mémoire double* p = new double; Cette ligne est courte, mais plusieurs opérations importantes s’y produisent. Il est donc utile de l’examiner étape par étape. Demander de la mémoire avec new Commençons par la partie droite de l’instruction : L’opérateur new demande qu’une zone de mémoire suffisamment grande pour contenir un objet de type double soit réservée dynamiquement. Sur notre machine : sizeof(double) = 8 octets. Une zone permettant de stocker un double est donc obtenue dans la mémoire dynamique. Mais connaître la taille de cette zone ne suffit pas. Il faut également savoir où elle se trouve. L’opérateur new fournit donc l’adresse de la zone obtenue. Conserver l’adresse obtenue Revenons maintenant à l’instruction complète : double* p = new double; déclare un pointeur vers un double. L’adresse fournie par new double est placée dans ce pointeur. Nous pouvons donc représenter schématiquement l’opération ainsi : Deux objets distincts interviennent donc : • le double créé dynamiquement et désigné par p.
Accueil
2026 – C++ Partie III L’allocation dynamique de mémoire double* p = new double; effectue deux opérations conceptuellement différentes : • new double obtient de la mémoire pour un double et fournit son adresse ; • cette adresse est conservée dans le pointeur p. Le pointeur et l’objet pointé sont deux objets différents. Observons les deux objets Nous pouvons maintenant reprendre nos instruments de mesure : double* p = new double; std::cout << « Valeur de p :  » << p << ’\n’; std::cout << « Adresse de p :  » << &p << ’\n’; std::cout << « Valeur de *p :  » << *p << ’\n’; Valeur de p : 0x1007b1ea0 Adresse de p : 0x16fdfee60 Program ended with exit code: 0 La valeur de p correspond à l’adresse du double obtenu dynamiquement. L’adresse &p correspond, quant à elle, à l’emplacement du pointeur lui-même 1 . ∗p=7.99 est la valeur de l’objet dynamique. Fig. 1.1 – Schéma, pile et tas. 1. Dans notre expérience, les adresses du pointeur local et de l’objet créé dynamiquement sont très éloignées. Cela est cohérent avec le fait qu’ils appartiennent à des régions de mémoire gérées différemment. La distance exacte entre ces régions dépend du système et de l’exécution.
Accueil
2026 – C++ Partie III L’allocation dynamique de mémoire Fig. 1.2 – Différentes valeurs obtenues. A part la valeur 7.99 les autres vont naturellement changés d’une machine à une autre. Libérer la mémoire avec delete Nous avons créé dynamiquement un objet de type double avec : double* p = new double; Contrairement à une variable locale ordinaire, l’objet créé par new ne disparaît pas simplement parce que l’exécution quitte le bloc dans lequel l’allocation a été demandée. Lorsque nous n’avons plus besoin de cet objet, nous devons libérer la mémoire qui lui avait été attribuée. Pour cela, le C++ fournit l’opérateur delete : L’opérateur delete libère l’objet dynamique dont l’adresse est contenue dans p. double* p = new double; std::cout << « Valeur de p :  » << p << ’\n’; std::cout << « Adresse de p :  » << &p << ’\n’; std::cout << « Valeur de *p :  » << *p << ’\n’; Après l’exécution de : le double créé dynamiquement n’existe plus. La mémoire qu’il occupait peut désormais être réutilisée. Que devient le pointeur après delete ? Considérons le programme suivant : double* p = new double; std::cout << « Valeur de p std::cout << « Adresse de p std::cout << « Valeur de *p
Accueil
2026 – C++ Partie III L’allocation dynamique de mémoire détruit l’objet dynamique désigné par p et libère la mémoire qui lui avait été attribuée. Elle ne détruit cependant pas le pointeur p lui-même. Le pointeur est toujours une variable locale et continue donc d’exister. Dans notre expérience, nous avons constaté que sa valeur n’avait même pas changé après l’exécution de delete. Avant et après delete, p contenait encore la même adresse. Mais cette adresse ne désigne désormais plus un objet que nous avons le droit d’utiliser. Un pointeur qui contient l’adresse d’un objet qui n’existe plus est appelé un pointeur pendant, ou La situation peut être représentée schématiquement ainsi : | adresse | ———–> objet détruit Le pointeur existe toujours, mais l’adresse qu’il contient ne doit plus être utilisée pour accéder à Que se passe-t-il si nous utilisons encore *p ? Nous avons volontairement effectué l’expérience suivante : std::cout << *p << ’\n’; Lors de notre exécution, le programme a affiché une valeur telle que :
Accueil
2026 – C++ Partie III L’allocation dynamique de mémoire Cette valeur 2 ne possède aucune signification particulière. Après delete, l’objet désigné auparavant par p n’existe plus. dans cette situation provoque un comportement indéfini. Cela signifie que le langage C++ ne garantit plus le comportement du programme. Celui-ci peut afficher une valeur apparemment quelconque, sembler fonctionner normalement, s’arrêter brutalement ou présenter un autre comportement. Une erreur de gestion de mémoire ne provoque donc pas nécessairement une erreur immédiatement visible. C’est précisément ce qui rend certaines erreurs liées aux pointeurs particulièrement difficiles à Le pointeur nul : nullptr Lorsque le pointeur ne doit plus désigner aucun objet, nous pouvons lui affecter la valeur spéciale : Nous pouvons donc écrire : La première instruction détruit l’objet dynamique. La seconde modifie le pointeur afin d’indiquer explicitement qu’il ne désigne plus aucun objet. Il est important de comprendre que : ne libère aucune mémoire. La mémoire a déjà été libérée par : L’affectation de nullptr sert uniquement à ne plus conserver dans p l’adresse devenue invalide. Déréférencer un pointeur nul 2. La valeur affichée est extrêmement petite mais reste représentable par un double.
Accueil
2026 – C++ Partie III L’allocation dynamique de mémoire std::cout << *p << ’\n’; l’exécution s’arrête avec l’erreur : Thread 1: EXC_BAD_ACCESS Le pointeur ne désignait plus aucun objet et nous avons pourtant essayé d’accéder à l’objet pointé Déréférencer un pointeur nul constitue également un comportement indéfini. L’erreur EXC_BAD_ACCESS est le comportement que nous avons observé dans notre expérience ; elle n’est pas imposée par le langage C++. Nous pouvons maintenant résumer le cycle élémentaire d’une allocation dynamique : double* p = new double; // utilisation de *p Les différentes étapes sont : allocation −→ utilisation −→ libération −→ pointeur nul Dans l’exemple précédent : • p contient l’adresse de l’objet dynamique ; • &p représente l’adresse du pointeur lui-même ; • *p permet d’accéder à l’objet désigné par le pointeur ; • delete p détruit l’objet dynamique désigné par p, mais ne détruit pas le pointeur ; • après delete, le pointeur contient encore l’ancienne adresse et devient un pointeur • p = nullptr indique explicitement que le pointeur ne désigne plus aucun objet. Allocation dynamique d’un tableau L’opérateur new permet d’allouer dynamiquement un objet unique mais il est également possible de demander de la mémoire pour plusieurs objets du même type.
Accueil
2026 – C++ Partie III L’allocation dynamique de mémoire Pour cela, nous utilisons la notation : double* p = new double[n]; L’opérateur new[] réserve une zone permettant de contenir n objets de type double dont l’adresse du premier élément est conservée dans le pointeur p. Des objets contigus en mémoire Commençons par une petite allocation : double* p = new double[n]; std::cout << « &p[0] :  » << &p[0] << ’\n’; std::cout << « &p[1] :  » << &p[1] << ’\n’; std::cout << « &p[2] :  » << &p[2] << ’\n’; std::cout << « &p[3] :  » << &p[3] << ’\n’; std::cout << « &p[4] :  » << &p[4] << ’\n’; Dans notre expérience, nous avons obtenu des adresses de la forme : Nous constatons que deux éléments successifs sont séparés de : Or nous avions mesuré : Les éléments du tableau sont donc placés les uns à la suite des autres dans une zone contiguë de Une allocation beaucoup plus importante Effectuons maintenant une expérience à une autre échelle : std::size_t n = 100’000’000; double* p = new double[n]; Nous demandons de la place pour cent millions de double, donc la quantité de mémoire nécessaire aux éléments est : 100 000 000 × 8 = 800 000 000 octets. Cela représente environ :
Accueil
2026 – C++ Partie III L’allocation dynamique de mémoire Immédiatement après l’allocation, sans avoir encore écrit dans le tableau, le Moniteur d’activité de notre système indiquait pourtant seulement environ : pour notre processus. Utiliser réellement la mémoire Nous avons ensuite rempli l’ensemble du tableau : for (std::size_t i = 0; i < n; i++) Après cette opération, le Moniteur d’activité indiquait environ : Cette valeur est très proche des quelque 763 Mio nécessaires aux cent millions de double, auxquels s’ajoute la mémoire utilisée par le reste du processus. L’expérience montre une propriété importante des systèmes modernes utilisant la mémoire virtuelle : réserver un espace d’adressage et utiliser effectivement la mémoire correspondante ne sont pas exactement la même chose. La simple allocation d’une grande zone ne conduit donc pas nécessairement à une consommation immédiatement équivalente de mémoire physique. Libérer un tableau dynamique Un tableau créé avec new[] doit être libéré avec delete[] : Il faut donc respecter les deux associations : les objets du tableau dynamique n’existent plus et la zone qui leur avait été attribuée redevient disponible pour l’allocateur. Comme précédemment, nous avons ensuite écrit : afin que le pointeur ne conserve pas l’adresse devenue invalide.
Accueil
2026 – C++ Partie III L’allocation dynamique de mémoire La mémoire libérée ne disparaît pas nécessairement immédiatement Après delete[], nous avons observé que le Moniteur d’activité continuait à indiquer une consommation voisine de Cela ne signifie pas que le tableau existe encore. La mémoire a été rendue disponible pour l’allocateur du processus, mais celui-ci peut conserver cette mémoire afin de satisfaire efficacement de futures allocations. Le compteur fourni par le système d’exploitation ne constitue donc pas une mesure directe de la mémoire occupée par les objets C++ actuellement vivants. Réutilisation de la zone libérée Nous avons finalement effectué une nouvelle allocation de même taille : double* q = new double[n]; std::cout << « &q[0] :  » << &q[0] << ’\n’; Avant la libération du premier tableau, nous avions mesuré : Après sa destruction, la nouvelle allocation a donné : Dans notre expérience, l’allocateur a donc réutilisé exactement la même zone mémoire. Le C++ ne garantit pas qu’une nouvelle allocation recevra la même adresse qu’une allocation précédemment libérée. C’est le comportement que nous avons observé dans cette expérience. Il montre cependant très concrètement que la mémoire libérée par delete[] peut être réutilisée pour satisfaire une nouvelle allocation.
Accueil
2026 – C++ Partie III L’allocation dynamique de mémoire • L’instruction : double* p = new double[n]; crée dynamiquement un tableau de n objets de type double. Les éléments sont stockés de manière contiguë et p contient l’adresse du premier élément. • Un tableau créé avec new[] doit être détruit avec : • Après sa destruction, la mémoire qu’il utilisait redevient disponible pour de nouvelles ne libère pas la mémoire : elle indique simplement que le pointeur p ne désigne plus Pile et tas : deux gestions différentes de la mémoire Nous avons maintenant rencontré deux manières très différentes de gérer la mémoire pendant l’exécution d’un programme. La pile (stack) est notamment utilisée pour gérer les appels de fonctions et, généralement, leurs variables locales. Cette gestion est automatique : lorsqu’une fonction se termine, les informations associées à son appel ne sont plus nécessaires et l’espace correspondant peut être réutilisé. Le programmeur n’a donc pas à libérer explicitement cette mémoire. Le tas (heap) permet au contraire d’obtenir de la mémoire dynamiquement pendant l’exécution du programme. Avec new ou new[], nous pouvons demander une quantité de mémoire qui n’était pas nécessairement connue lors de la compilation. Cette mémoire n’est pas libérée automatiquement à la fin du bloc dans lequel l’allocation a été effectuée. Sa libération doit être provoquée explicitement avec delete ou delete[]. La pile est donc particulièrement adaptée aux objets dont la durée de vie suit naturellement l’exécution des fonctions, tandis que le tas permet de contrôler plus librement la taille et la durée de vie des objets créés pendant l’exécution. • La pile est gérée automatiquement et suit principalement la logique des appels de fonctions. • Le tas permet une allocation dynamique dont la taille et la durée de vie peuvent être déterminées pendant l’exécution. • Ces deux mécanismes utilisent la mémoire de manière différente et répondent à des besoins différents.
Accueil
2026 – C++ Partie III L’allocation dynamique de mémoire Que se passe-t-il si une allocation échoue ? Jusqu’à présent, toutes nos demandes de mémoire dynamique ont réussi. Nous avons par exemple double* p = new double[n]; et l’opérateur new[] nous a fourni une zone suffisamment grande pour contenir les n objets demandés. Mais la quantité de mémoire disponible n’est évidemment pas infinie. Il est donc possible de demander une allocation que le système ne peut pas satisfaire. Le comportement de new Dans sa forme habituelle, si new ne parvient pas à effectuer l’allocation demandée, il ne retourne normalement pas un pointeur nul. Il signale l’échec en lançant une exception de type : Cette exception est définie dans l’en-tête : Si cette exception n’est pas traitée par le programme, celui-ci se termine. Nous étudierons les exceptions plus en détail ultérieurement. Pour le moment, il suffit de retenir qu’une allocation dynamique peut échouer et que l’opérateur new dispose d’un mécanisme permettant de signaler cet échec. • La mémoire dynamique disponible n’est pas infinie. • Une instruction telle que : double* p = new double[n]; n’est donc pas assurée de réussir. • Dans sa forme habituelle, lorsqu’une allocation ne peut pas être effectuée, new signale l’échec en lançant une exception de type std::bad_alloc. Les fuites de mémoire Nous avons vu qu’une allocation dynamique effectuée avec : double* p = new double[n]; doit être libérée lorsqu’elle n’est plus nécessaire :
Accueil
2026 – C++ Partie III L’allocation dynamique de mémoire Mais que se passe-t-il si cette instruction est oubliée ? Perdre l’adresse d’une allocation Considérons une fonction très simple : std::size_t n = 1000; double* p = new double[n]; // utilisation du tableau Le pointeur p est une variable locale. Lorsque l’exécution quitte la fonction, sa durée de vie se termine automatiquement. Le tableau créé par : possède cependant une durée de vie différente. Il ne disparaît pas simplement parce que le pointeur Nous avons donc la situation suivante : Pendant l’exécution de la fonction : | adresse | ———–> [ double ][ double ] … [ double ] Après la fin de la fonction : [ double ][ double ] … [ double ] ne permet de retrouver La mémoire dynamique est toujours allouée, mais nous avons perdu l’adresse permettant de la retrouver. Cette situation est appelée une fuite de mémoire, ou memory leak. Pourquoi est-ce un problème ? Une petite fuite isolée peut sembler sans importance. Mais supposons que la fonction précédente soit appelée de nombreuses fois : for (int i = 0; i < 10000; i++)
Accueil
2026 – C++ Partie III L’allocation dynamique de mémoire À chaque appel, une nouvelle zone de mémoire est allouée. Comme aucune de ces zones n’est libérée, la quantité de mémoire inutilisable par le programme augmente progressivement. Le programme peut ainsi consommer de plus en plus de mémoire alors même qu’il n’est plus capable d’accéder aux données responsables de cette consommation. Sur un système disposant de peu de mémoire, ce phénomène peut devenir rapidement critique. Une fuite peut également être provoquée par une affectation Il n’est pas nécessaire qu’un pointeur disparaisse pour perdre une allocation. Considérons : double* p = new double[1000]; La seconde instruction ne libère pas le tableau. Elle remplace simplement l’adresse contenue dans p par nullptr. L’adresse du tableau est alors perdue. Il n’est donc plus possible d’écrire correctement : pour libérer l’ancienne allocation, puisque p ne contient plus son adresse. ne libère jamais à elle seule la mémoire dynamique désignée auparavant par p. Lorsque p désigne encore une allocation dont nous sommes responsables, il faut d’abord la libérer : La fuite peut rester invisible Une fuite de mémoire ne provoque pas nécessairement une erreur immédiate. Le programme peut continuer à fonctionner normalement pendant un certain temps. Le problème apparaît progressivement lorsque les allocations perdues s’accumulent et consomment une partie croissante de la mémoire C’est pourquoi les fuites de mémoire peuvent être particulièrement difficiles à détecter dans les programmes de longue durée.
Accueil
2026 – C++ Partie III L’allocation dynamique de mémoire • Une fuite de mémoire se produit lorsqu’une zone de mémoire dynamique reste allouée alors que le programme a perdu le moyen de la retrouver et donc de la libérer. double* p = new double[1000]; // l’adresse est perdue provoque une fuite de mémoire. double* p = new double[1000]; // utilisation du tableau libère correctement la mémoire avant de faire perdre au pointeur l’adresse de l’ancienne • Une fuite de mémoire persiste pendant la vie du processus. Lorsque le processus se termine, le système d’exploitation récupère sa mémoire. Dans ce chapitre, nous avons étudié les mécanismes fondamentaux de l’allocation dynamique de mémoire. Lorsqu’une variable possède une durée de vie automatique, sa création et sa destruction sont prises en charge automatiquement. La mémoire dynamique permet au contraire de réserver, pendant l’exécution du programme, une quantité de mémoire qui peut dépendre des besoins du permet de créer dynamiquement un objet et retourne son adresse. double* p = new double; Le pointeur p contient alors l’adresse de l’objet créé dynamiquement. Lorsque cet objet n’est plus nécessaire, sa mémoire doit être libérée : Pour plusieurs objets contigus, nous utilisons : double* p = new double[n];
Accueil
2026 – C++ Partie III L’allocation dynamique de mémoire et la libération correspondante doit être effectuée avec : Il faut donc respecter les associations : new[] ←→ delete[] Après delete ou delete[], l’objet dynamique n’existe plus. Le pointeur, en revanche, peut continuer à exister et conserver l’ancienne adresse : il devient alors un pointeur pendant. permet d’indiquer explicitement que le pointeur ne désigne plus aucun objet. Enfin, oublier de libérer une allocation dynamique avant d’en perdre l’adresse provoque une fuite de mémoire. Cette fuite peut rester invisible pendant longtemps et devenir problématique lorsqu’elle se • La mémoire dynamique est obtenue pendant l’exécution du programme. • new crée dynamiquement un objet et retourne son adresse. • new[] permet de créer dynamiquement plusieurs objets contigus. • Toute allocation effectuée avec new doit être associée à une libération avec delete. • Toute allocation effectuée avec new[] doit être associée à une libération avec delete[]. • delete et delete[] détruisent les objets dynamiques concernés, mais ne détruisent pas le pointeur lui-même. • Un pointeur qui conserve l’adresse d’un objet détruit est un pointeur pendant. • nullptr permet de représenter explicitement un pointeur qui ne désigne aucun objet. • Perdre l’adresse d’une allocation sans l’avoir libérée provoque une fuite de mémoire. • Une allocation dynamique peut échouer lorsque la demande ne peut pas être satisfaite.
Accueil
P3-Chap-01- L’allocation dynamique de mémoire
Accueil
Termes à ajouter au glossaire
Contenu

Inscription

×
Cancel