FAQ C++Consultez toutes les FAQ
Nombre d'auteurs : 34, nombre de questions : 368, dernière mise à jour : 14 novembre 2021 Ajouter une question
Cette FAQ a été réalisée à partir des questions fréquemment posées sur les forums de http://www.developpez.com et de l'expérience personnelle des auteurs.
Je tiens à souligner que cette FAQ ne garantit en aucun cas que les informations qu'elle propose sont correctes ; les auteurs font le maximum, mais l'erreur est humaine. Cette FAQ ne prétend pas non plus être complète. Si vous trouvez une erreur ou si vous souhaitez devenir rédacteur, lisez ceci.
Sur ce, nous vous souhaitons une bonne lecture.
- 12.1. Les pointeurs intelligents (2)
- Comment allouer de la mémoire ?
- Que se passe-t-il si new ne parvient pas à allouer la mémoire demandée ?
- Pourquoi utiliser new plutôt que malloc ?
- Comment libérer de la mémoire ?
- Que se passe-t-il si je fais un delete sur un pointeur qui vaut NULL ?
- Puis-je utiliser free() pour libérer un pointeur alloué par new ?
- Pourquoi devrais-je utiliser array/vector<T> au lieu de gérer la mémoire d'un tableau moi-même ?
- Comment allouer dynamiquement un tableau ?
- Comment libérer un tableau alloué dynamiquement ?
- Comment allouer dynamiquement un tableau à plusieurs dimensions ?
- Comment libérer un tableau à plusieurs dimensions alloué dynamiquement ?
- Comment réallouer/agrandir une zone mémoire ?
- Comment récupérer la taille d'un tableau dynamique ?
- Peut-on déréférencer un pointeur NULL ?
- Est-il possible de forcer new à allouer la mémoire à une adresse précise ?
- Qu'est-ce que « placement new » et dans quels cas l'utilise-t-on ?
- Comment gérer proprement des allocations/désallocations de ressources ? Le RAII !
- Comment se gère la constance avec les pointeurs ?
- Quels sont les dangers liés à l'utilisation des pointeurs ?
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.
À noter que la fonction malloc() fonctionne toujours en C++ comme en C.
En cas d'échec d'allocation de new, une exception std::bad_alloc est levée.
Cependant certains compilateurs un peu anciens (tels que Visual C++ 6) se contentent de renvoyer un pointeur nul (zéro donc). Soyez donc vigilant !
Sur les types des bases (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.
Code c++ : | Sélectionner tout |
1 2 3 4 | MaClasse *BonObjet, *MauvaisObjet; MauvaisObjet = malloc(sizeof(MaClasse)); // alloue de la mémoire mais n'appelle pas le constructeur BonObjet = new MaClasse; // alloue de la mémoire et appelle le constructeur. |
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.
À 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.
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.
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.
Il est évidemment possible de gérer la mémoire, et en particulier des tableaux, à la main en C++. Cependant, malgré l'apparente simplicité qu'il peut y avoir à utiliser la paire de mots-clés new[] et delete[], il devient très vite complexe, voire parfois impossible, d'écrire des codes corrects et maintenables qui procèdent de la sorte.
La difficulté est liée aux exceptions. Ces dernières cassent le déroulement linéaire d'un programme et lorsqu'une d'entre- elles survient, il est difficile de correctement libérer la mémoire. En effet, imaginez une exception survenant durant la création d’objets intermédiaires : vous devez libérer les objets précédemment alloués uniquement, ce qui vous donne autant de chemins d’exécution que d’objets intermédiaires. C’est particulièrement flagrant lorsqu’on alloue « à la C » un tableau à deux dimensions. La complexité est énorme et surtout, elle n'est pas nécessaire lorsque le C++ propose tous les outils pour l'éviter.
Pour cela, il faut utiliser des capsules RAII. Vous l'avez déjà certainement fait : tous les conteneurs standards (mais aussi ceux de Boost, et bien d'autres) œuvrent comme des capsules RAII. En particulier, nous nous intéresserons ici à : std::vector, std::array (C++11), voire std::string pour les chaines. Leur usage garantit la libération des ressources à la sortie de la fonction, et ce quel que soit le chemin de sortie (return en fin de fonction, exception, return en milieu de fonction, etc.). Le code se restreint donc au code métier, les détails d’allocation mémoire sont masqués, et la correction des allocations/désallocations est assurée.
Pour les matrices, vous pouvez utiliser des bibliothèques spécialisées (Eigen, NewMat, Armadillo, etc.) qui prendront en compte d'autres aspects que la simple allocation pour offrir des performances bien au-delà des codes « naïfs ».
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 [].
Code c++ : | Sélectionner tout |
1 2 3 | int * tableau = new int[ 10] ; // alloue un tableau de 10 entiers delete [] tableau; // ATTENTION : ne pas oublier les crochets [] |
Libérer un tableau alloué dynamiquement en C++ se fait grâce à l'opérateur delete [].
Code c++ : | Sélectionner tout |
1 2 | int * tableau = new int[ 10 ]; delete [] 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 ?.
Code c++ : | Sélectionner tout |
delete [] mesObjets ; // libère le tableau mesObjets
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 à deux dimensions, il faut créer un tableau de pointeurs sur des entiers et initialiser chaque pointeur avec un tableau d'entiers.
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | #include <algorithm> // pour fill_n // créer un tableau d'entiers à 2 dimensions [ 10 ][ 20 ] const int dim1 = 10; // taille de la dimension 1 const int dim2 = 20; // taille de la dimension 2 int dim_allouee = 0; // nombre d'éléments alloués avec succès sur la dimension 2 int * * Tab = 0; // tenter d'allouer la taille demandée dim1 x dim2 try { // dimension 1 : tableau de 10 pointeurs vers des tableaux d'entiers Tab = new int * [ dim1 ]; // initialiser les 10 pointeurs à 0 (NULL) std::fill_n( Tab, dim1, static_cast<int*>( 0 ) ); // dimension 2 : les tableaux d'entiers for ( dim_allouee = 0; dim_allouee < dim1; ++dim_allouee) { Tab[ dim_allouee ] = new int[ dim2 ]; } } catch ( const std::bad_alloc & ) // erreur d'allocation { // désallouer tout ce qui a été alloué avec succès for ( int i = 0; i < dim_allouee; ++i ) { delete [] Tab[ i ]; } delete [] Tab; throw; } |
En revanche quel que 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 ?).
Finalement, dans cet exemple, le code est correct car nous utilisons un type de base. En effet, la construction d'un entier ne va pas lancer d'exception. En créant un tableau d'instances d'une classe, il est possible que le constructeur de la classe lance une exception. Dans ce cas, vous aurez une grande difficulté à gérer efficacement la mémoire. C'est pourquoi il est conseillé d'utiliser array/vector<T>.
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 | int** Tab; int i; for(i=0; i<10; i++) { delete [] Tab[i]; } delete []Tab; |
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 ?
Il est possible de récupérer la taille des tableaux statiques avec l'opérateur sizeof.
Code c++ : | Sélectionner tout |
1 2 3 4 | #include <iostream> int tab[50]; std::cout << sizeof(tab) / sizeof(int); // affiche "50" |
Code c++ : | Sélectionner tout |
1 2 3 4 5 | template<typename T, size_t N> inline size_t length_of(T(&)[N]) { return N; } |
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 | template <std::size_t N> struct array {char value[N];}; template<typename T, std::size_t N> array<N> length_of_helper(T (&)[N]); #define length_of(array) (sizeof length_of_helper(array).value) |
Code c++ : | Sélectionner tout |
1 2 3 4 | #include <iostream> int* tab = new int[50]; std::cout << sizeof(tab); // affiche "4" |
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 ?
Code c++ : | Sélectionner tout |
1 2 3 4 5 | #include <iostream> #include <vector> std::vector<int> tab(50); std::cout << tab.size(); // affiche "50" |
La réponse est NON.
NULL étant une adresse non valide, *NULL donne une référence impossible.
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() :
Code c++ : | Sélectionner tout |
1 2 | void* raw = allocate(sizeof(Foo)); // ligne 1 Foo* p = new(raw) Foo(); // ligne 2 |
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | class Pool { public: void* alloc(size_t nbytes); void dealloc(void* p); private: // données membres... }; void* Pool::alloc(size_t nbytes) { // code d'allocation } void Pool::dealloc(void* p) { // code de libération } |
Code c++ : | Sélectionner tout |
1 2 3 4 | Pool pool; // ... void* raw = pool.alloc(sizeof(Foo)); Foo* p = new(raw) Foo(); |
Code c++ : | Sélectionner tout |
Foo* p = new(pool.alloc(sizeof(Foo))) Foo();
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 où le destructeur ne fait que désallouer la mémoire, ce genre de techniques peut être utile.
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 deux lignes suivantes à la définition de la classe Pool
Code c++ : | Sélectionner tout |
1 2 3 4 | inline void* operator new(size_t nbytes, Pool& pool) { return pool.alloc(nbytes); } |
Code c++ : | Sélectionner tout |
new(pool) Foo()
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 :
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 | void sample(Pool& pool) { Foo* p = new(pool) Foo(); // ... p->~Foo(); // appel explicite du destructeur pool.dealloc(p); // libération explicite de la mémoire } |
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. Étant 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 où le constructeur lance une exception. Ce code ressemble à ceci :
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 | Foo* p; // ne pas intercepter les exceptions lancées par l'allocateur void* raw = operator new(sizeof(Foo)); // intercepter toute exception lancée par le constructeur try { p = new(raw) Foo(); // appel du constructeur avec 'raw' comme pointeur 'this' } catch (...) { // le constructeur a lancé une exception operator delete(raw); throw; // relancer l'exception du constructeur } |
Code c++ : | Sélectionner tout |
1 2 3 4 5 | void* raw = operator new(sizeof(Foo), pool); // cette fonction renvoie simplement "pool.alloc(sizeof(Foo))" Foo* p = new(raw) Foo(); // si la ligne précédente provoque une exception, <span style="font-family: monospace; padding: 2px; background: #ddd; display: inline-block">pool.<span style="">dealloc</span><span style="color: black;">(</span>raw<span style="color: black;">)</span></span> n'est PAS appelée |
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
Code c++ : | Sélectionner tout |
new(pool) Foo()
Code c++ : | Sélectionner tout |
1 2 3 4 | void operator delete(void* p, Pool& pool) { pool.dealloc(p); } |
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | Foo* p; // ne pas intercepter les exceptions lancées par l'allocateur void* raw = operator new(sizeof(Foo), pool); // le code précédent renvoie simplement « pool.alloc(sizeof(Foo)) » // intercepter toute exception lancée par le constructeur try { p = new(raw) Foo(); // appel du constructeur avec raw en tant que this } catch (...) { // le constructeur lance une exception operator delete(raw, pool); // la ligne « magique » throw; // relance l'exception } |
Problème n° 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 :
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | void operator delete(void* p) { if (p != NULL) { Pool* pool = /* quelqu'un qui détient le 'Pool*' associé */; if (pool == null) free(p); else pool->dealloc(p); } } |
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 | void* operator new(size_t nbytes) { if (nbytes == 0) nbytes = 1; // chaque alloc obtient donc une adresse différente void* raw = malloc(nbytes); // ... associer le Pool* à Null a 'raw'... return raw; } |
Code c++ : | Sélectionner tout |
<void*,Pool*>
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 :
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | void* operator new(size_t nbytes) { if (nbytes == 0) nbytes = 1; // ainsi toutes les allocations possèdent une adresse distincte void* ans = malloc(nbytes + 4); // on alloue 4 bytes supplémentaires *(Pool**)ans = NULL; // on utilise NULL pour le new global return (char*)ans + 4; // on cache le Pool* à l'utilisateur } void* operator new(size_t nbytes, Pool& pool) { if (nbytes == 0) nbytes = 1; // ainsi toutes les allocations possèdent une adresse distincte void* ans = pool.alloc(nbytes + 4); // on alloue 4 bytes supplémentaires *(Pool**)ans = &pool; // stocker le Pool* ici return (char*)ans + 4; // on cache le Pool* à l'utilisateur } void operator delete(void* p) { if (p != NULL) { p = (char*)p - 4; // on récupère le Pool* Pool* pool = *(Pool**)p; if (pool == null) free(p); // note : 4 bytes de moins que le p original else pool->dealloc(p); // note : 4 bytes de moins que le p original } } |
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 :
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 | #include <new> // On doit inclure <new> pour utiliser « placement new » #include "Fred.h" // Déclaration de la classe Fred void someCode() { char memory[sizeof(Fred)]; // Ligne 1 void* place = memory; // Ligne 2 Fred* f = new(place) Fred(); // Ligne 3 (voir "DANGER" ci-dessous) // Les deux pointeurs f et place sont maintenant égaux } |
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 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-la uniquement si vous avez besoin de placer un objet à une adresse mémoire précise. Utilisez-la 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.
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. |
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 | void someCode() { char memory[sizeof(Fred)]; void* p = memory; Fred* f = new(p) Fred(); // ... f->~Fred(); // Appel explicite au destructeur } |
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 œuvre 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 cela 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 substitut de choix à la clause finally d'autres langages, ou fournie par certains compilateurs 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 pu ê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. À l'inverse, si la ressource a été correctement allouée, alors sa responsabilité est confiée à l'objet, qui la libérera correctement quoi qu'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 davantage la 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 :
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // Enregistre un fichier donné dans la base de données // lève une exception en cas d'échec void WriteFileInDatabase( DataBase & Db, std::string Filename ) { // tenter d'ouvrir le fichier Datafile file; if ( !file.Open( Filename ) ) { // impossible d'ouvrir le fichier... throw InvalidArgument(); } // verrouiller la base de données if ( !Db.Lock() ) { // on ne peut pas utiliser la base... throw DatabaseError(); } // écrire le fichier dans la base Db.WriteData( file.ReadData() ); // tout s'est bien passé : libérer les ressources Db.Unlock(); file.Close(); } |
Voici maintenant la même fonction, modifiée pour gérer correctement ces erreurs :
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | // Enregistre un fichier donné une la base de données // lève une exception en cas d'échec void WriteFileInDatabase( DataBase & Db, std::string Filename ) { // tenter d'ouvrir le fichier Datafile file; if ( !file.Open( Filename ) ) { // impossible d'ouvrir le fichier... throw InvalidArgument(); } // verrouiller la base de données if ( !Db.Lock() ) { // fermer le fichier file.Close(); // on ne peut pas utiliser la base... throw DatabaseError(); } // écrire le fichier dans la base try { Db.WriteData( file.ReadData() ); } catch ( ... ) { // libérer les ressources en cas d'erreur Db.Unlock(); file.Close(); throw; // relancer l'exception } // tout s'est bien passé : libérer les ressources Db.Unlock(); file.Close(); } |
Si l'on crée une petite classe utilitaire DBLock qui gère le verrouillage de la base :
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | struct DBLock { DBLock(DataBase& DB) : DB_(DB) { DB_.Lock(); } ~DBLock() { DB_.Unlock(); } private : DataBase& DB_; }; |
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 | void WriteFileInDatabase( DataBase & Db, std::string Filename ) { // ouvrir le fichier Datafile file( Filename ); // fichier ouvert : vérrouiller la base DBLock lock( Db ); // base verrouillée : écrire les données du fichier Db.WriteData( file.ReadData() ); } // libération des ressources automatiques |
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 :
- Chapitre 14.4 de « Le langage C++ » de Bjarne Stroustrup : « Gestion des ressources ».
- Chapitre 6 de « C++ in action » : « Managing resources » ( http://www.relisoft.com/book/tech/5resource.html).
- Les scope guards d'Andrei Alexandrescu.
- The law of big two ( http://www.artima.com/cppsource/bigtwo.html).
Pour les pointeurs, le mot-clé const sera interprété différemment en fonction de l'endroit où il se trouve.
S'il se trouve soit à gauche du type (ce qui est une écriture régulièrement utilisée, mais qui représente en fait une règle particulière) ou entre le type et l'étoile indiquant que l'on a un pointeur, il s'applique à l'objet pointé.
Ainsi le code suivant sera refusé :
Code c++ : | Sélectionner tout |
1 2 3 4 | int i = 3; const int * ptr = &i; // revient au même que int const * ptr = &i; ++*ptr; // ptr pointe sur un objet constant, on ne peut donc pas // modifier l'objet |
Ainsi, si le code suivant sera accepté :
Code c++ : | Sélectionner tout |
1 2 3 4 | int i = 4; int * const ptr = &i; // C'est le pointeur qui est constant, pas l'objet ++*ptr; cout << *ptr; // affiche "5" |
Code c++ : | Sélectionner tout |
1 2 3 4 5 | int i = 4; int *const ptr = &i; // OK, on initialise ptr pour qu'il prenne l'adresse de i int j = 5; ptr = &j; // Refusé : on ne peut pas changer l'adresse mémoire vers laquelle // pointe ptr |
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 | int i = 4; int const * const ptr = &i; // revient au même que const int * const ptr = &i; ++*ptr; // Refusé : l'objet pointé par ptr est constant int j = 5; ptr = &j; // Refusé : on ne peut pas changer l'adresse mémoire vers laquelle // pointe ptr |
Les dangers liés à l'utilisation des pointeurs ont de multiples facettes, qui ont toutes une cause commune : un pointeur peut virtuellement représenter n'importe quelle adresse mémoire.
Bien souvent, l'utilisation des pointeurs est associée à la gestion dynamique de la mémoire (création d'un objet avec new ou d'un groupe d'objets new[], destruction respective avec delete et delete[]).
Le premier risque que l'on encourt, s'il n'y a pas un delete ou un delete[] qui correspond à chaque new ou new[], est ce que l'on appelle la fuite mémoire, qui survient lorsque l'on perd toute possibilité d'accéder à une adresse sans avoir pris la précaution de détruire l'objet qui s'y trouve, et qui finira tôt ou tard par saturer n'importe quel système aussi puissant soit-il.
De plus, si on manipule un pointeur qui pointe vers un objet détruit, ou vers une adresse invalide, on va envoyer le compilateur « cueillir les marguerites » et on se retrouvera, toujours trop tard, avec une fantastique erreur de segmentation, et, si on n'y prend pas garde, on peut facilement essayer de libérer la mémoire d'un objet qui a déjà été détruit, ce qui ne vaut guère mieux.
La plus grande prudence s'impose donc lorsque l'on décide de manipuler des pointeurs.
C++ fournit heureusement une série de moyens permettant d'en éviter l'usage au maximum :
- la classe string pour remplacer les chaines « C style » (cf. Les chaînes de caractères) ;
- les différents conteneurs représentant les concepts classiques de tableaux d'éléments contigus en mémoire, de pile, de file, de liste et d'arbre binaire (associatif ou non) (cf. Quel conteneur choisir pour stocker mes objets ?).
Il faut aussi noter qu'il existe des pointeurs intelligents qui permettent de remédier à certains de ces inconvénients.
Proposer une nouvelle réponse sur la FAQ
Ce n'est pas l'endroit pour poser des questions, allez plutôt sur le forum de la rubrique pour çaLes 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 © 2024 Developpez Developpez LLC. Tous droits réservés Developpez LLC. Aucune reproduction, même partielle, ne peut être faite de ce site et 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.