FAQ C++Consultez toutes les FAQ

Nombre d'auteurs : 35, nombre de questions : 368, dernière mise à jour : 23 mai 2017  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.


SommaireGestion dynamique de la mémoire (21)
précédent sommaire suivant
 

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.

Mis à jour le 20 avril 2003 LFE

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 !

Mis à jour le 22 novembre 2004 Aurelien.Regat-Barrel

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.

Mis à jour le 20 avril 2003 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.

À 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.

Mis à jour le 20 avril 2003 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.

Mis à jour le 20 avril 2003 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.

Mis à jour le 20 avril 2003 LFE

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 ».

Mis à jour le 25 juillet 2015 LittleWhite Luc Hermitte white_tentacle

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 []
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 ?.

Mis à jour le 22 novembre 2004 Aurelien.Regat-Barrel LFE

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;
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 ?.

Code c++ : Sélectionner tout
delete [] mesObjets ; // libère le tableau mesObjets

Mis à jour le 22 novembre 2004 Aurelien.Regat-Barrel LFE

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; 
}
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), et new renvoie à la place un pointeur nul. Si vous utilisez un tel compilateur il convient de modifier le code proposé en conséquence.
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>.

Mis à jour le 2 janvier 2007 Bob

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;
Voir aussi Comment détruire les pointeurs d'un conteneur ?

Mis à jour le 20 avril 2003 Bob

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 ?

Mis à jour le 17 octobre 2005 Laurent Gomila

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"
Ou encore via cette fonction template :

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; 
}
Si vous voulez obtenir la taille sous forme d'une constante connue à la compilation, vous pouvez utiliser cette variante :

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)
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.

Code c++ : Sélectionner tout
1
2
3
4
#include <iostream> 
  
int* tab = new int[50]; 
std::cout << sizeof(tab); // affiche "4"
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 ?

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"

Mis à jour le 3 février 2007 Aurelien.Regat-Barrel Laurent Gomila

La réponse est NON.
NULL étant une adresse non valide, *NULL donne une référence impossible.

Mis à jour le 10 février 2004 LFE

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
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 :

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 
}
Maintenant, l'utilisateur devrait pouvoir obtenir un Pool (appelé pool), à partir duquel il pourra allouer des objets de la façon suivante :

Code c++ : Sélectionner tout
1
2
3
4
Pool pool; 
// ... 
void* raw = pool.alloc(sizeof(Foo)); 
Foo* p = new(raw) Foo();
ou encore :

Code c++ : Sélectionner tout
Foo* p = new(pool.alloc(sizeof(Foo))) Foo();
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 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); 
}
Maintenant, lorsque le compilateur rencontrera une instruction

Code c++ : Sélectionner tout
new(pool) Foo()
l'opérateur new que l'on vient de définir passera sizeof( Foo ) et new poolen 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 :

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 
}
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. É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 
 }
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.

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: #eee">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()
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.

Code c++ : Sélectionner tout
1
2
3
4
void operator delete(void* p, Pool& pool) 
{ 
    pool.dealloc(p); 
}
Maintenant, le compilateur intégrera automatiquement les appels au constructeur dans un bloc try/catch.

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 
}
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 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); 
    } 
}
Si free() était le désallocateur normal, l'approche la plus sûre serait de remplacer aussi l'opérateur newpar quelque chose qui utiliserait malloc(). Le code remplaçant l'opérateur global new ressemblerait alors à quelque chose comme ce qui suit :

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; 
}
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

Code c++ : Sélectionner tout
<void*,Pool*>
En d'autres mots, il suffit de construire une table associative où 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 :

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 
    } 
}
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.

Mis à jour le 19 mars 2004 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 :

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 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 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.
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.

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 
}
C'est un des très rares cas d'appel explicite au destructeur. À la suite de ce sujet vous pouvez lire Est-il possible d'invoquer explicitement le destructeur d'une classe ?

Mis à jour le 30 août 2004 Cline

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();    
}
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 quoi qu'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 :

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();    
}
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 :

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_; 
};
La fonction précédente devient :

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
Le RAII est donc un idiome particulièrement puissant : il permet d'écrire un code plus simple, exception-safe et sans fuite 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 :


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
et, s'il est à droite de l'étoile, il indique que le pointeur ne pourra pas pointer vers un autre objet (sans pour autant assurer la constance de l'objet pointé).

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"
le code suivant sera refusé (car le pointeur ne peut pas être modifié) :
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
Et, bien sûr, il est possible de déclarer un pointeur constant sur un objet constant (ni l'objet, ni le pointeur ne peuvent être modifiés) :
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

Mis à jour le 6 juillet 2014 koala01

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 :


Il faut aussi noter qu'il existe des pointeurs intelligents qui permettent de remédier à certains de ces inconvénients.

Mis à jour le 6 juillet 2014 koala01

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 ça


Réponse à la question

Liens sous la question
précédent sommaire suivant
 

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 © 2017 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.

 
Contacter le responsable de la rubrique C++