| ||
auteur : LFE | ||
En C, l'allocation dynamique de mémoire se faisait avec la fonction malloc(). En C++, l'allocation de mémoire se fait
avec l'opérateur new.
A noter que la fonction malloc() fonctionne toujours en C++ comme en C.
| ||
lien : Comment libérer de la mémoire ? |
| ||
auteur : Aurélien Regat-Barrel | ||
En cas d'échec d'allocation de new, une exception std::bad_alloc est levée.
Cependant certains compilateurs un peu anciens (tel que Visual C++ 6) se contentent de renvoyer un pointeur nul (zéro donc).
Soyez donc vigilant!
|
| ||
auteur : LFE | ||
Sur les types des base (int, char, float, ...) l'intérêt n'est pas énorme. Par contre, sur les classes,
plutôt que simplement allouer la place mémoire pour stocker l'objet, il y a un appel au constructeur qui
se fait et qui permet donc d'initialiser correctement l'objet.
De même, delete appelle le destructeur de la classe, alors que free() ne le fait pas.
|
| ||
auteur : LFE | ||
En C, la libération de mémoire se fait avec la fonction free(). En C++, la libération d'un pointeur
se fait avec l'opérateur delete.
A noter que la fonction free() fonctionne toujours en C++ comme en C.
Le delete fonctionne de la façon suivante : il appelle (implicitement) le destructeur de la classe et puis libère le pointeur.
| ||
lien : Comment allouer de la mémoire ? |
| ||
auteur : LFE | ||
Il ne se passe rien du tout. On peut considérer qu'un delete sur un pointeur NULL est purement et simplement ignoré, ce qui permet
d'éviter de devoir faire ce contrôle.
|
| ||
auteur : LFE | ||
La réponse est négative. Un pointeur alloué par new doit être libéré par un delete et un pointeur alloué
par malloc() doit être libéré par free().
Il est tout à fait possible que ce type d'allocation/libération fonctionne parfaitement sur un compilateur
mais donnera des résultats tout à fait imprévisibles sur un autre.
|
| ||
auteurs : LFE, Aurélien Regat-Barrel | ||
Allouer un tableau dynamiquement en C++ se fait grâce à l'opérateur new []. Ce pointeur doit être libéré avec l'opérateur delete []. Un des avantages de new par rapport au malloc du C est que le constructeur par défaut des objets alloués est automatiquement appelé. Il en est de même pour leur destructeur lors de l'appel à delete [].
En C++ on préfère utiliser un std::vector issu de la STL car ce dernier gère seul l'allocation, la réallocation (pour grossir) ainsi que la libération de la mémoire. Pour plus d'informations sur std::vector, lire Comment créer et utiliser un tableau avec std::vector ?.
| ||
lien : Comment libérer un tableau alloué dynamiquement ? |
| ||
auteurs : LFE, Aurélien Regat-Barrel | ||
Libérer un tableau alloué dynamiquement en C++ se fait grâce à l'opérateur delete [].
delete [] se charge d'appeler le destructeur de chaque objet du tableau. Notez qu'il n'y a pas besoin de spécifier le nombre d'éléments à libérer. Cette information est conservée au moment de l'allocation du tableau avec new []. Une erreur grave et fréquente est d'oublier les crochets après le mot-clé delete. Malheureusement cette erreur n'est pas détectée à la compilation, mais seulement à l'exécution pour les meilleurs compilateurs lors d'une exécution en mode de débogage. Cette détection se traduit généralement par un message de corruption de la mémoire. En effet, appeler delete au lieu de delete [] provoque un comportement indéfini par la norme, qui se traduit souvent par un plantage pur et simple, ou par un fonctionnement anormal du programme (mémoire qui semble être modifiée toute seule, ...). Donc en cas de problème de ce genre, vérifiez vos delete []! Pour cette raison, et bien d'autres, on évite d'allouer des tableaux en C++. On préfère utiliser std::vector qui fait tout correctement à notre place. Pour plus d'informations lire Comment créer et utiliser un tableau avec std::vector ?.
| ||
lien : Comment allouer dynamiquement un tableau ? |
| ||
auteur : Bob | ||
Pour chaque dimension il faut créer un tableau de pointeurs sur des éléments de la dimension suivante. Par exemple, pour un tableau d'entiers à 2 dimensions, il faut créer un tableau de pointeurs sur des entiers et initialiser chaque pointeur avec un tableau d'entiers.
Le code précédent évite les fuites de mémoire en cas d'erreur d'allocation donnant lieu à une exception std::bad_alloc. Cette exception n'est pas déclenchée par les compilateurs un peu anciens (comme Visual C++ 6 par exemple), et new renvoie à la place un pointeur nul. Si vous utilisez un tel compilateur il convient de modifier le code proposé en conséquences. En revanche quelque soit votre compilateur le fait d'appeler delete [] sur un pointeur nul est sans conséquence (voir Que se passe-t-il si je fais un delete sur un pointeur qui vaut NULL ?). |
| ||
auteur : Bob | ||
Voir aussi Comment détruire les pointeurs d'un conteneur ?.
|
| ||
auteur : Laurent Gomila | ||
En C, on pouvait utiliser realloc pour agrandir un espace mémoire alloué dynamiquement. Cependant cette fonction est à éviter en
C++ : elle n'est garantie de fonctionner qu'avec la mémoire allouée via malloc (voir Pourquoi utiliser new plutôt que malloc ?).
Pour agrandir une zone (généralement un tableau) allouée via l'opérateur new il faudra faire la manipulation à la main : - Allouer un nouvel espace mémoire de la taille souhaitée - Y copier son contenu - Libérer l'ancien espace mémoire
Tout ceci étant fastidieux et difficile à maintenir, en C++ on utilise simplement std::vector lorsqu'il s'agit de tableaux, qui fera
tout cela automatiquement et bien plus efficacement. Voir Comment créer et utiliser un tableau avec std::vector ?.
|
| |||||
auteurs : Laurent Gomila, Aurélien Regat-Barrel | |||||
Il est possible de récupérer la taille des tableaux statiques avec l'opérateur sizeof.
Ou encore via cette fonction template :
Si vous voulez obtenir la taille sous forme d'une constante connue à la compilation, vous pouvez utiliser cette variante :
Pour un tableau dynamique alloué via new par contre, l'écriture utilisant sizeof ne renverra pas le résultat escompté. En effet, cela
renverra la taille du pointeur (généralement 4 octets sur plateforme 32 bits) et non la taille de la zone pointée.
On préfèrera donc utiliser la seconde écriture, à base de template, qui provoquera elle une erreur de compilation si l'on tente de lui passer
en paramètre un pointeur.
Vous l'aurez compris, il est donc impossible de récupérer la taille d'un tableau dynamique alloué avec new. Pour cela il faudra stocker séparément sa taille,
ou mieux : utiliser std::vector. Voir Comment créer et utiliser un tableau avec std::vector ?.
| |||||
lien : Les utilisateurs de Visual C++ 2005 peuvent aussi se référer à la macro _countof |
| ||
auteur : LFE | ||
La réponse est NON. NULL étant une adresse non valide, *NULL donne une référence impossible. |
| |||||||||||||||||||
auteur : Marshall Cline | |||||||||||||||||||
Oui. La bonne nouvelle est que ces "pools de mémoire" sont utiles dans un certain nombre de situations.
La mauvaise nouvelle est qu'il va
falloir descendre dans le "comment cela fonctionne" avant de voir comment on l'utilise.
Si vous ne savez pas comment fonctionnent les "pools de mémoire", ce sera chose réglée bientôt.
Avant tout, il faut savoir qu'un allocateur de mémoire est supposé retourner une zone de mémoire non initialisée, il n'est pas supposé
créer des objets. En particulier, l'allocateur de mémoire n'est pas supposé mettre à jour le pointeur virtuel ou n'importe quelle autre
partie de l'objet, étant donné que c'est le travail du constructeur qui est exécuté juste après l'allocation de la mémoire. En démarrant
avec une simple fonction d'allocation de mémoire, allocate(), nous utilisons placement new pour construire un objet dans
cette mémoire. En d'autres mots, ce qui suit est moralement équivalent à new Foo() :
En supposant que l'on ait utilisé placement new et que l'on ait survécu au code précédent, l'étape suivante est de transformer
l'allocateur de mémoire en un objet. Ce type d'objet est appelé un pool mémoire. Cela permet aux utilisateurs d'avoir plusieurs
pools à partir desquels la mémoire peut être allouée. Chacun de ces pools mémoire allouera une grande quantité de mémoire en utilisant
un appel système spécifique (mémoire partagée, mémoire persistante, etc ....) et le distribuera en petites quantités à la demande.
Notre pool mémoire ressemblera à quelque chose de ce type :
Maintenant, l'utilisateur devrait pouvoir obtenir un Pool (appelé pool), à partir duquel il pourra allouer des objets de la façon suivante :
ou encore :
La raison pour laquelle il serait bon de transformer Pool en une classe est que cela permet à l'utilisateur de créer N pools mémoire
différents, plutôt que d'avoir un gros pool partagé par tous les utilisateurs. Cela permet aux utilisateurs de faire un tas de choses
plus ou moins drôles.
Par exemple, si l'on dispose de fonctions système permettant d'allouer et de libérer une énorme quantité de mémoire,
la totalité de la mémoire pourrait être allouée dans un pool, et ensuite ne faire aucun delete des allocations faites dans ce pool, pour
finalement libérer la totalité du pool en une fois.
Ou il serait possible de créer une zone de mémoire partagée (où le système
d'exploitation procure de la mémoire partagée entre différents processus) et que ce pool alloue des morceaux de mémoire partagée plutôt
que de la mémoire locale au processus. La plupart des systèmes supportent une fonction alloca() qui alloue un bloc de mémoire sur la pile, plutôt que dans le tas. Bien entendu, ce bloc de mémoire est libéré à la fin de la fonction, faisant disparaître le besoin de faire des delete explicites. Quelqu'un pourrait utiliser alloca() pour attribuer au Pool sa mémoire, et que toutes les petites allocations dans ce pool agiraient comme si elles étaient faites sur la pile : elles disparaîtraient à la fin de la fonction. Bien sûr, les destructeurs ne seraient pas appelés dans n'importe lequel de ces cas, et si celui-ci devait faire des choses non triviales, il vous serait impossible d'utiliser ces techniques, mais dans le cas ou le destructeur ne fait que désallouer la mémoire, ce genre de techniques peut être utiles.
Maintenant que l'on a inclus les quelques lignes de code nécessaires à l'allocation dans la classe Pool, l'étape suivante est de changer
la syntaxe d'allocation des objets.
Le but est de transformer une allocation au format inhabituel (new(pool.alloc(sizeof(Foo))))
en quelque chose de tout à fait classique (new(pool)). Pour y arriver, il faut ajouter les 2 lignes suivantes à la définition
de la classe Pool
Maintenant, lorsque le compilateur rencontrera une instruction
l'opérateur new que l'on vient de définir passera sizeof( Foo ) et pool en tant que paramètres, et la seule fonction qui
manipulera le pool sera ce nouvel opérateur new.
Passons maintenant à la destruction de l'objet Foo. Il est à noter que l'approche brutale qui est parfois utilisée avec placement
new est d'appeler explicitement le destructeur et d'ensuite désallouer la mémoire :
Ce code présente plusieurs problèmes, mais qui peuvent tous être réglés.
Il y aura une perte de mémoire si le constructeur lance une exception
La syntaxe de destruction/désallocation n'est pas conforme à ce que les programmeurs ont l'habitude de voir, ce qui va sûrement les
perturber fortement.
L'utilisateur doit se rappeler d'une façon ou d'une autre des associations pool/objet.
Etant donné que le code qui alloue est souvent
situé dans une autre fonction que celle qui libère, le programmeur devra manipuler deux pointeurs (un pour la classe et un pour le pool),
ce qui peut devenir rapidement indigeste (par exemple, un tableau d'objets Foo qui seraient alloués dans des pools différents)
Nous allons régler ces problèmes.
Problème n° 1 : la fuite mémoire
Quand on utilise l'opérateur new habituel, le compilateur génère un bout de code particulier pour gérer le cas ou le constructeur lance
une exception. Ce code ressemble à ceci :
Le point à remarquer est que le compilateur libère la mémoire si le constructeur lance une exception. Mais dans le cas du
"new avec paramètres" (appelé communément "new avec placement"), le compilateur ne sait pas quoi faire si une exception est lancée,
il ne fait donc rien.
Le but est donc de faire faire au compilateur quelque chose de semblable à ce qu'il fait avec l'opérateur new global. Heureusement,
c'est simple : quand le compilateur rencontre
il cherche un opérateur delete correspondant. S'il en trouve un, il fait un wrapping équivalent à celui de l'appel du constructeur dans
un bloc try/catch. Nous devons juste fournir un opérateur delete avec la signature suivante. Attention de ne pas se tromper ici, car si
le second paramètre a un type différent de celui de l'opérateur new, le compilateur n'émettra aucun message, il ignorera simplement le
bloc try/catch quand l'utilisateur effectuera l'allocation.
Maintenant, le compilateur intégrera automatiquement les appels au constructeur dans un bloc try/catch.
En d'autres mots, l'ajout de l'opérateur delete avec la signature ad hoc règle automatiquement le problème de fuite de mémoire.
Problème 2 : se souvenir des associations objet/pool
Ce problème est réglé par l'ajout de quelques lignes de code à un endroit. En d'autres mots, nous allons ajouter ces lignes de code à
un endroit (le fichier header du pool), ce qui va simplifier par la même occasion un certain nombre d'appels.
L'idée est d'associer de manière implicite un Pool* avec chaque allocation. Le Pool* associé à l'allocateur global pourrait être NULL,
mais conceptuellement on peut dire que chaque allocation a un Pool* associé.
Ensuite, nous remplaçons l'opérateur delete global de façon qu'il examine le Pool* associé, et s'il est non NULL, il appellera la
fonction de libération associée. Par exemple, si le désallocateur normal utilisait free(), le remplacement pour l'opérateur delete
global ressemblerait à quelque chose comme ceci :
Si free() était le désallocateur normal, l'approche la plus sûre serait de remplacer aussi l'opérateur new par quelque chose qui utiliserait
malloc(). Le code remplaçant l'opérateur global new ressemblerait alors à quelque chose comme ce qui suit :
Le dernier problème est d'associer un Pool* à une allocation. Une approche, utilisée dans au moins un produit commercial, est d'utiliser
un
En d'autres mots, il suffit de construire une table associative ou les clés sont les pointeurs alloués et les valeurs sont les Pool*
associés. Pour différentes raisons, il est essentiel que les paires clé/valeur soient insérées à partir de l'opérateur new. En
particulier, il ne faut pas insérer une paire de clé/valeur à partir de l'opérateur new global. La raison est la suivante, faire cela
créerait un problème circulaire : étant donné que std::map utilise plus que probablement l'opérateur new global, à chaque insertion d'un
élément serait appelé, pour insérer une nouvelle entrée, ce qui mène directement à une récursion infinie.
Même si cette technique exige une recherche dans le std::map à chaque libération, elle semble avoir des performances suffisantes, du moins
dans la plupart des cas.
Une autre approche, plus rapide, mais qui peut utiliser plus de mémoire, et est un peu plus complexe, est de spécifier un Pool* juste avant
toutes les allocations. Par exemple, si nbytes vaut 24, c'est-à-dire que l'appelant veut allouer 24 bytes, on alloue 28 bytes
(ou 32, si la machine aligne les doubles ou les long long sur 8 bytes), spécifie le Pool* dans les 4 premiers bytes, et retourne
le pointeur avec un décalage de 4 bytes (ou 8) suivant l'architecture.
Pour la libération du pointeur, l'opérateur delete libère
la mémoire en tenant compte du décalage de 4 (ou 8) bytes. Si Pool* vaut NULL, on utilise free(), sinon pool->dealloc().
Le paramètre passé à free() et à pool->dealloc() est le pointeur décrémente de 4 ou 8 bytes du paramètre original.
Pour un alignement de 4 bytes, le code pourrait ressembler à ceci :
Naturellement, ces derniers paragraphes sont uniquement valables si on peut modifier l'opérateur new global, ainsi que delete. S'il n'est pas possible de changer le comportement de ces opérateurs globaux, les trois quarts du texte qui précède restent valables. |
| ||
auteur : Marshall Cline | ||
On peut utiliser placement new dans de nombreux cas. L'utilisation la plus simple permet de placer un objet à une adresse mémoire
précise. Pour cela, l'adresse choisie est représentée par un pointeur que l'on passe à la partie new de la new expression:
La ligne 1 crée un tableau dont la taille en octets est sizeof(Fred), tableau donc assez grand pour que l'on puisse y stocker un objet
de type Fred. La ligne 2 crée un pointeur place qui pointe sur le premier octet de cette zone mémoire (les programmeurs C expérimentés auront noté que cette deuxième étape n'était pas strictement nécessaire ; en fait, elle est là juste pour rendre le code plus lisible). Pour faire simple, on peut de dire de la ligne 3 qu'elle appelle le constructeur Fred::Fred(). Dans ce constructeur, this et place ont la même valeur. Le pointeur f retourné sera donc lui aussi égal à place.
Conseil
N'utilisez pas cette syntaxe du "placement new" si vous n'en avez pas l'utilité. Utilisez-là uniquement si vous avez besoin de placer un
objet à une adresse mémoire précise. Utilisez-là par exemple si le matériel sur lequel vous travaillez dispose d'un périphérique de
gestion du temps mappé en mémoire à une adresse précise, et que vous voulez placer un objet Clock à cette adresse.
Danger
Il est de votre entière responsabilité de garantir que le pointeur que vous passez à l'opérateur "placement new" pointe sur une zone
mémoire assez grande et correctement alignée pour l'objet que vous voulez y placer. Ni le compilateur ni le runtime de votre système
ne vérifient que c'est effectivement le cas. Vous pouvez vous retrouver dans une situation fâcheuse si votre classe Fred nécessite un
alignement sur une frontière de 4 octets et que vous avez utilisé une zone mémoire qui n'est pas correctement alignée
(si vous ne savez pas ce qu'est "l'alignement", alors SVP n'utilisez pas la syntaxe du "placement new"). On vous aura prévenu.
La destruction de l'objet ainsi créé est aussi sous votre entière responsabilité. Pour détruire l'objet, il faut appeler explicitement son
destructeur.
C'est un des très rares cas d'appel explicite au destructeur. A ce sujet vous pouvez lire Est-il possible d'invoquer explicitement le destructeur d'une classe ?.
|
| ||
auteurs : Laurent Gomila, Aurélien Regat-Barrel, Luc Hermitte | ||
Toute mémoire allouée dans vos programmes avec new doit être libérée à un moment ou un autre avec delete. Cette bonne règle de programmation peut vite devenir contraignante et parfois difficile à mettre en oeuvre dans la pratique.
Les pointeurs intelligents (smart pointers en anglais) sont des objets se comportant comme des pointeurs classiques (mimétisme dans la syntaxe et certaines sémantiques), mais qui offrent en plus des fonctionnalités intéressantes permettant une gestion quasi automatique de la mémoire (en particulier de sa libération). Leur syntaxe est très proche de celle des pointeurs classiques (grâce à la surcharge des opérateurs *, ->, etc...)., mais ils utilisent en interne divers mécanismes (comptage de références, ...) qui permettent de déceler qu'un objet n'est plus utilisé, auquel cas le pointeur intelligent se charge de le détruire ce qui permet d'éviter les fuites de mémoire. Utiliser des pointeurs intelligents est généralement une très bonne idée, en particulier lors de l'écriture de code susceptible d'être interrompu par des exceptions (soit presque tout le temps!). Si tel est le cas, ceux-ci ne manqueront pas de libérer la mémoire qui leur est associée lors de leur destruction (suite à une exception ou non), ce qu'il n'est pas possible d'assurer sans multiplier les blocs try...catch dans son code.
Comme on peut le voir, cette gestion des exceptions est assez polémique, et surtout elle est totalement inélégante. Pensez qu'il faudrait procéder ainsi partout où il y a un new! Voici un autre moyen de rendre l'appel à new exception safe :
La nouvelle version est tout aussi sûre, mais elle est bien plus triviale à mettre en place. Attention cependant à std::auto_ptr dont l'usage est un peu spécial en particulier en cas de copie de pointeur (à ce sujet lire Pourquoi faut-il se méfier de std::auto_ptr ?). Pour cette raison il est conseillé de plutôt se tourner vers une autre classe de pointeurs intelligents déjà toute faite (rien ne sert de réinventer la roue!) telle que boost::shared_ptr. Cependant attention, les pointeurs intelligents n'échappent pas à la règle que ce qui a été alloué avec new doit être libéré avec delete et ce qui a été alloué avec new [] doit l'être avec delete []. Or std::auto_ptr tout comme boost::shared_ptr appellent l'opérateur delete. Donc ces pointeurs intelligents ne doivent pas être utilisés avec des tableaux. Pour cela il faudra utiliser une autre classe telle que boost::shared_array. Mais n'oubliez pas que std::vector est là pour éviter ces tracasseries avec les tableaux ce qui fait une bonne raison de plus de l'utiliser à la place des tableaux classiques (lire à ce sujet Comment créer et utiliser un tableau avec std::vector ?). Pour plus d'informations, vous pouvez lire Comment utiliser les pointeurs intelligents de Boost ?. |
| ||
auteurs : Laurent Gomila, Aurélien Regat-Barrel | ||
Il existe un type standard de pointeurs intelligents : std::auto_ptr déclaré dans l'en-tête <memory>. Mais en pratique son utilisation peut s'avérer problématique si l'on n'a pas pris le temps de bien comprendre son fonctionnement. C'est pourquoi cette classe est souvent déconseillée, surtout aux débutants. Contrairement à beaucoup de pointeurs intelligents, std::auto_ptr n'utilise pas un mécanisme de comptage de référence afin de partager un même objet pointé par plusieurs pointeurs intelligents (l'objet est détruit lorsque plus aucun pointeur ne l'utilise). std::auto_ptr utilise un mécanisme plus simple mais plus risqué : il n'y a qu'un seul propriétaire de l'objet pointé et ce dernier est détruit lorsque son propriétaire l'est. Si une copie est effectuée d'un auto_ptr vers un autre, il y a transfert de propriété, c'est à dire que la possession de l'objet pointé sera transmise du premier au second, et le premier deviendra alors un pointeur invalide (NULL).
L'exemple précédent peut paraître simple à éviter, et pourtant, de nombreuses copies peuvent être faites à votre insu :
std::auto_ptr est donc à proscrire dans bien des cas (lors de copies ou de passages à des fonctions). En particulier il ne faut pas l'utiliser avec les conteneurs standards non plus (std::vector, ...). Un autre point important est que std::auto_ptr appelle l'opérateur delete et non delete [] ce qui en interdit l'usage avec des pointeurs retournés par new [] (tableaux). On peut tout de même envisager de l'utiliser pour rendre une fonction exception-safe à peu de frais, comme cela est expliqué dans la question Qu'est-ce qu'un pointeur intelligent ?. Mais on préfèrera utiliser les pointeurs intelligents de Boost par exemple (voir Comment utiliser les pointeurs intelligents de Boost ?). |
| ||||
auteurs : Aurélien Regat-Barrel, Laurent Gomila, JolyLoic, Luc Hermitte | ||||
RAII signifie Resource Acquisition Is Initialization (acquisition de ressources lors de l'initialisation). Il s'agit d'un idiome
de programmation consistant à manipuler une ressource quelconque (mémoire, fichier, mutex, connexion à une base de données, ...) au
moyen d'une variable locale qui va acquérir cette ressource lors de son initialisation et la libérer lors de sa destruction.
Le C++ est particulièrement bien adapté à la mise en oeuvre de cet idiome car c'est un langage qui détruit de manière déterministe les
objets automatiques. Autrement dit, le RAII est rendu possible en C++ par le fait que les classes disposent d'un destructeur qui est
appelé dès qu'un objet sort de son bloc de portée. On place dans ce destructeur le code nécessaire à la libération de la ressource
acquise. On est ainsi assuré que la ressource sera bien libérée, sans que l'utilisateur n'ait eu à appeler de fonction close() ou free(),
et celà même en cas d'une exception.
Le RAII est une technique très puissante, qui simplifie grandement la gestion des ressources en général, de la mémoire en particulier.
Cet idiome permet tout simplement de créer du code exception-safe sans aucune fuite de mémoire. C'est donc une alternative de
choix à la clause finally d'autres langages, ou fournie par certains compilos C++. De manière concrète, le RAII peut se résumer en
"tout faire dans le constructeur et le destructeur". Si la ressource n'a pas pû être acquise dans le constructeur, alors on lève en général
une exception (voir Que faire en cas d'échec du constructeur ?) ; ainsi l'objet responsable de la durée de vie de la ressource n'est pas
construit. A l'inverse, si la ressource a été correctement allouée alors sa responsabilité est confiée à l'objet, qui la libérera
correctement quoiqu'il arrive dans son destructeur. Cela simplifie donc beaucoup le code : il n'y
a pas d'allocation de ressource, pas de test de réussite, et pas de libération explicite : tout est fait automatiquement, de manière
certaine. L'utilisation même des objets est simplifiée en encapsulant d'avantage le gestion des ressources et donc en améliorant
l'abstraction.
Soit la fonction suivante, qui permet d'écrire le contenu d'un fichier dans une base de données :
Ce code semble correct, mais en réalité il ne l'est pas : que se passe-t-il si une erreur se produit et que l'une de nos exceptions est
lancée ? S'assure-t-on de toujours fermer le fichier et déverrouiller la base de données quoiqu'il arrive ? La réponse est non, tout ceci
n'est fait que si tout se déroule bien et que la fonction arrive à son terme. Voici maintenant la même fonction, modifiée pour gérer correctement ces erreurs :
Ce code est exception-safe, c'est à dire qu'il ne provoque pas de fuite de ressources si une exception est levée. Mais à quel
prix ! Selon l'approche RAII, on peut modifier la classe Datafile pour qu'elle fasse le Open() dans son constructeur (avec levée
d'exception), et le Close() dans son destructeur. Si l'on crée une petite classe utilitaire DBLock qui gère le verrouillage de la base :
La fonction précédente devient :
Le RAII est donc un idiome particulièrement puissant : il permet d'écrire un code plus simple, exception-safe et sans fuites de
mémoire. Il est d'ailleurs utilisé intensivement dans la bibliothèque standard du C++ : gestion des fichiers
(std::fstream), des chaînes de caractères
(std::string), des tableaux dynamiques
(std::vector), des pointeurs (std::auto_ptr), ... C'est également le principe de base des pointeurs intelligents (voir Qu'est-ce qu'un pointeur intelligent ?), qui permettent d'envelopper toutes sortes de ressources.
On peut également citer quelques bonnes lectures sur le sujet :
|
Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2008 Developpez LLC. Tous droits réservés Developpez LLC. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents et images sans l'autorisation expresse de Developpez LLC. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.