Vous devez avoir un compte Developpez.com et être connecté pour pouvoir participer aux discussions.

Identifiez-vous
Identifiant
Mot de passe
Mot de passe oublié ?
Créer un compte

Vous n'avez pas encore de compte Developpez.com ? L'inscription est gratuite et ne vous prendra que quelques instants !

Je m'inscris !

Developpez.com

C++

Choisissez la catégorie, puis la rubrique :

logo
Sommaire > Gestion dynamique de la mémoire
        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 ?
        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 ?
        Qu'est-ce qu'un pointeur intelligent ?
        Pourquoi faut-il se méfier de std::auto_ptr ?
        Comment gérer proprement des allocations / désallocations de ressources ? Le RAII !



Comment allouer de la mémoire ?
Créé le 20/04/2003[haut]
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 ?

Que se passe-t-il si new ne parvient pas à allouer la mémoire demandée ?
Mise à jour le 22/11/2004[haut]
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 !


Pourquoi utiliser new plutôt que malloc ?
Créé le 20/04/2003[haut]
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.
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.

Comment libérer de la mémoire ?
Créé le 20/04/2003[haut]
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 ?

Que se passe-t-il si je fais un delete sur un pointeur qui vaut NULL ?
Créé le 20/04/2003[haut]
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.


Puis-je utiliser free() pour libérer un pointeur alloué par new ?
Créé le 20/04/2003[haut]
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.


Comment allouer dynamiquement un tableau ?
Mise à jour le 22/11/2004[haut]
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 [].
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 ?.

lien : Comment libérer un tableau alloué dynamiquement ?

Comment libérer un tableau alloué dynamiquement ?
Mise à jour le 22/11/2004[haut]
auteurs : LFE, Aurélien Regat-Barrel
Libérer un tableau alloué dynamiquement en C++ se fait grâce à l'opérateur delete [].
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 ?.
delete [] mesObjets ; // libère le tableau mesObjets
lien : Comment allouer dynamiquement un tableau ?

Comment allouer dynamiquement un tableau à plusieurs dimensions ?
Mise à jour le 02/01/2007[haut]
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.

#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;
}
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 ?).


Comment libérer un tableau à plusieurs dimensions alloué dynamiquement ?
Créé le 20/04/2003[haut]
auteur : Bob
int** Tab;
int i;

for(i=0; i<10; i++) {
    delete [] Tab[i];
}

delete []Tab;


Comment réallouer / agrandir une zone mémoire ?
Créé le 17/10/2005[haut]
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 ?.


Comment récupérer la taille d'un tableau dynamique ?
Mise à jour le 03/02/2007[haut]
auteurs : Laurent Gomila, Aurélien Regat-Barrel
Il est possible de récupérer la taille des tableaux statiques avec l'opérateur sizeof.

#include <iostream>

int Tab[50];
std::cout << sizeof(Tab) / sizeof(int); // affiche "50"
Ou encore via cette fonction template :


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 :

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.

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

#include <iostream>
#include <vector>

std::vector<int> Tab(50);
std::cout << Tab.size(); // affiche "50"
lien : Les utilisateurs de Visual C++ 2005 peuvent aussi se référer à la macro _countof

Peut-on déréférencer un pointeur NULL ?
Créé le 10/02/2004[haut]
auteur : LFE
La réponse est NON.
NULL étant une adresse non valide, *NULL donne une référence impossible.


Est-il possible de forcer new à allouer la mémoire à une adresse précise ?
Créé le 19/03/2004[haut]
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() :
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 :
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 :
Pool pool;
// ...
void* raw = pool.alloc(sizeof(Foo));
Foo* p = new(raw) Foo();
ou encore :
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 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
inline void* operator new(size_t nbytes, Pool& pool)
{
   return pool.alloc(nbytes);
}
Maintenant, lorsque le compilateur rencontrera une instruction
new(pool) Foo()
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 :
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. 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 :
principe
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.
principe
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, pool.dealloc(raw) 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
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.
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.
principe
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 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 :
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 new par quelque chose qui utiliserait malloc(). Le code remplaçant l'opérateur global new ressemblerait alors à quelque chose comme ce qui suit :
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
<void*,Pool*>
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 :
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.


Qu'est-ce que "placement new" et dans quels cas l'utilise-t-on ?
Mise à jour le 30/08/2004[haut]
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 :
#include <new>       // On doit inclure <new.h> 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 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.
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. A ce sujet vous pouvez lire Est-il possible d'invoquer explicitement le destructeur d'une classe ?.


Qu'est-ce qu'un pointeur intelligent ?
Mise à jour le 18/04/2005[haut]
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.
// Test1 est exception safe sans utilisation de pointeur intelligent
void Test1()
{
    int * ptr = new int;
    try
    {
        // code pouvant lever une exception
    }
    catch ( ... ) // gestion approximative...
    {
        // libérer le pointeur
        delete ptr;
        // relancer l'exception
        throw;
    }
    // tout s'est bien passé, libérer la mémoire
    delete ptr;
}
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 :
#include <memory> // pour std::auto_ptr

// Test2 est exception safe en utilisant un pointeur intelligent
void Test2()
{
    // std::auto_ptr est la classe pointeur intelligent standard
    std::auto_ptr<int> smart_ptr = new int;

    // code pouvant lever une exception (rien d'autre !)
}
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 ?).

lien : faq Comment utiliser les pointeurs intelligents de Boost ?
lien : fr Présentation des pointeurs intelligents.

Pourquoi faut-il se méfier de std::auto_ptr ?
Créé le 22/11/2004[haut]
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).
int Test() 
{ 
    // ptr1 est le propriétaire
    std::auto_ptr<int> ptr1( new int );
    {
        // ptr2 devient le propriétaire
        std::auto_ptr<int> ptr2 = ptr1;
    }
    // ici ptr2 est détruit, le int est libéré !
    // ptr1 pointe désormais vers NULL
    *ptr1 = 10; // aille aille aille...
}
L'exemple précédent peut paraître simple à éviter, et pourtant, de nombreuses copies peuvent être faites à votre insu :
include <memory> 

// Ptr est passé par valeur, donc a priori aucun risque que le
// paramètre donné lors de l'appel ne soit modifié
void Test( std::auto_ptr<MaClasse> Ptr ) 
{ 
    // Mais ce qui va réellement se passer, c'est que Ptr va prendre
    // le contrôle de l'objet pointé, et le détruire à la fin de la fonction ! 
} 

MaClasse* Ptr1 = new MaClasse; 
Test( std::auto_ptr<MaClasse>( Ptr1 ) ); 
// Ici Ptr1 ne pointe plus sur une zone valide, il a été détruit suite à l'appel 

std::auto_ptr<MaClasse> Ptr2( new MaClasse ); 
Test( Ptr2 ); 
// Ici Ptr2 ne pointe plus sur la donnée originale
// il pointe sur NULL et la donnée a été détruite dans la fonction
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 ?).


Comment gérer proprement des allocations / désallocations de ressources ? Le RAII !
Créé le 17/10/2005[haut]
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 :

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

// Enregistre un fichier donné une la base de donnée
// 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 :

struct DBLock
{
    DBLock(DataBase& DB) : DB_(DB)
    {
        DB_.Lock();
    }

    ~DBLock()
    {
        DB_.Unlock();
    }

private :

    DataBase& DB_;
};
La fonction précédente devient :

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

lien : fr Présentation des pointeurs intelligents.


Consultez les autres F.A.Q.


Valid XHTML 1.0 TransitionalValid CSS!

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.

Contacter le responsable de la rubrique C++

Partenaire : Hébergement Web