Opérateurs new/delete

    Le C++ fournit six surcharges de chacun des opérateurs new et delete. Ce sont toutes des fonctions globales, qui n'appartiennent donc pas à l'espace de nom std. Elles peuvent ou non utiliser des définitions de types, de classes, de fonctions ou de constantes : nothrow_t, nothrow, new_handler, set_new_handler qui nécessitent alors l'inclusion de l'entête <new> (la classe bad_alloc est, elle, déclarée dans <stdexcept>) :
 
namespace std 
{
    class bad_alloc;
    struct nothrow_t {};
    extern const nothrow_t nothrow;
    typedef void (*new_handler)();
    new_handler set_new_handler(new_handler new_p) throw();
}

    Les profils de ces différentes fonctions, donnés dans la norme, sont les suivants :
 
// Opérateurs new/delete de base

void* operator new    (std::size_t size) throw(std::bad_alloc);
void  operator delete (void* ptr)        throw();

// Opérateurs new[]/delete[] de base

void* operator new    [] (std::size_t size) throw(std::bad_alloc);
void  operator delete [] (void* ptr)        throw();

// Opérateurs new/delete avec placement

void* operator new       (std::size_t size, void* ptr) throw();
void  operator delete    (void* ptr,            void*) throw();
void* operator new    [] (std::size_t size, void* ptr) throw();
void  operator delete [] (void* ptr,            void*) throw();

// Opérateurs new/delete sans exception

void* operator new       (std::size_t size, const std::nothrow_t&) throw();
void  operator delete    (void* ptr,        const std::nothrow_t&) throw();
void* operator new    [] (std::size_t size, const std::nothrow_t&) throw();
void  operator delete [] (void* ptr,        const std::nothrow_t&) throw();

    Les deux premiers opérateurs permettent l'allocation/desallocation d'objets élémentaires en mémoire dynamique.

    Les deux opérateurs suivants permettent l'allocation/désallocation de tableaux d'objets en mémoire dynamique.

    Les quatre opérateurs suivants permettent la construction/destruction d'objets à des emplacements de mémoire quelconque sans réservation/libération d'espace.

    Les quatre derniers opérateurs sont équivalents aux quatre premiers sans lever d'exception en cas d'echec.

Opérateurs new/delete de base

Allocation d'objets en mémoire dynamique

     L'opérateur new de base permet d'allouer de l'espace en mémoire dynamique, c'est-à-dire dans le tas (heap en anglais), et renvoie l'adresse mémoire correspondante. Généralement, cette adresse peut être stockée dans un pointeur comme l'indique l'exemple suivant :
 
void Alloc (void) 
 { 
     int    * PtrInt   = new int; 
     char   * PtrChar  = new char; 
     CDuree * PtrDuree = new CDuree; 
     // ... 

 } // Alloc() 

    PtrInt est une variable qui reçoit l'adresse mémoire réservée par l'opérateur new pour y stocker un entier. De même, PtrChar reçoit l'adresse mémoire réservée pour stocker un caractère. Enfin, PtrDuree reçoit l'adresse mémoire réservée pour stocker un objet de la classe CDuree (supposée exister). Dans ce dernier cas cependant, l'opérateur new ne se contente pas d'allouer de la mémoire, il l'initialise aussi en appelant le constructeur par défaut [1] de la classe CDuree avant de renvoyer l'adresse mémoire. Si la classe ne possède pas un tel constructeur, cela provoque une erreur de compilation.

    Ces variables occupent elles-mêmes un espace mémoire, ici dans la pile d'exécution de la fonction Alloc(), comme le montre la séquence suivante, supposée à la suite dans la fonction Alloc() ci-dessus :
 
    cout << "sizeof (PtrInt)    = " << sizeof (PtrInt)    << endl; 
    cout << "sizeof (PtrChar)   = " << sizeof (PtrChar)   << endl; 
    cout << "sizeof (PtrCDuree) = " << sizeof (PtrCDuree) << endl; 

Sur une machine à adressage de 32 bits, les trois instructions afficheraient la même valeur 4, indépendamment de la taille de l'objet pointé. Il faut noter que l'utilisation d'un pointeur pour accéder à un caractère (supposé sur un octet) occasionne la perte de 80% de mémoire !

    Il est possible d'appeler un autre constructeur que celui par défaut lors de la réservation mémoire. Par analogie d'écriture, il est possible d'initialiser des variables des types de base, comme le montre la séquence suivante :
 
    int    * PtrInt   = new int  (50); 
    char   * PtrChar  = new char ('x'); 
    CDuree * PtrDuree = new CDuree (3600); 

    La zone mémoire pointée par PtrInt est initialisée à 50, celle pointée par PtrChar à 'x' et le constructeur de CDuree avec un paramètre est appelé.

Affichage des pointeurs

    Les adresses mémoire peuvent être affichées, comme ci-dessous :
 
     cout << "PtrInt   = " << PtrInt           << endl;
     cout << "PtrChar  = " << (void *) PtrChar << endl;
     cout << "PtrDuree = " << PtrDuree         << endl;

    Avec un adressage sur 32 bits, l'affichage pourrait être par exemple :
 
PtrInt   = 0xff3d422d
PtrChar  = 0xff3d4230
PtrDuree = 0xff3d4234

    Il faut noter l'écriture particulière permettant d'afficher le contenu de PtrChar : la surcharge de l'opérateur << utilisée pour un pointeur affiche toujours la valeur en hexadécimal sauf pour les pointeurs de caractères pour lesquels elle affiche la "chaîne de caractères" (au sens C, soit une NTCTS) pointée. Or ici, PtrChar ne pointe pas sur une chaîne. Il faut transtyper le pointeur en void * pour pouvoir afficher sa valeur (voir aussi l'opérateur static_cast).

Accès aux objets pointés

    Les objets dynamiques sont anonymes. Ils ne peuvent être désignés que par l'intermédiaire des pointeurs associés, comme :
 
     cout << "L'entier      = " << *PtrInt   << endl;
     cout << "Le caractère  = " << *PtrChar  << endl;
     cout << "La durée      = " << *PtrDuree << endl; // si la durée est affichable

Suppression des objets de la mémoire dynamique

    Au retour de la fonction Alloc(), les pointeurs, objets locaux sur la pile, sont automatiquement détruits. En revanche, l'espace de mémoire dynamique alloué n'est pas libéré, mais, faute de pointeurs, il n'est définitivement plus accessible. Il est donc perdu jusqu'à l'arrêt de l'aplication. Ce phénomène est appelé "fuite de mémoire" et est particulièrement fâcheux.

    La libération doit donc être explicitement effectuée, dès que l'objet n'est plus utilisé, et au plus tard avant de sortir de la fonction, au moyen de l'opérateur delete :
 
     delete PtrInt;
     delete PtrChar;
     delete PtrDuree;

} // Alloc()

    Outre la restitution de l'espace mémoire au gestionnaire de mémoire, l'opérateur delete appelle le destructeur des objets (ici ~CDuree()). Si un pointeur Ptr est déclaré pointer sur une classe de base CBase, si celle-ci est virtuelle (si elle contient au moins une fonction virtuelle), et si l'objet réellement pointé par Ptr est d'une classe dérivée de CBase et possédant un destructeur virtuel, c'est celui-ci qui est apelé (polymorphisme).

    L"utilisation de l'opérateur delete sur un pointeur nul est sans effet. En revanche, il ne faut surtout pas effectuer une opération delete sur un pointeur dont la valeur n'a pas été initialisée par l'opérateur new (directement ou non) :
 
     int i = 10;
     int * PtrI = &i;
     int * PtrJ = new int (10);
     int * PtrK = PtrJ;

     delete PtrI;     // a toutes les chances de provoquer une erreur d'exécution
     delete PtrK;     // correct

Attention : l'opérateur delete ne modifie pas la valeur du pointeur. Il appartient donc à l'utilisateur de veiller à ne plus l'utiliser.

Réutilisation d'un objet pointé

    Il est possible d'appeler un destructeur d'objet sans pour autant libérer l'espace mémoire. Cette opération présente cependant un danger, comme le montre l'exemple ci-dessous :
 
void Alloc (void)
{
    CDuree * PtrDuree = new CDuree (3600);
    // ...
    PtrDuree->~CDuree();   // appel explicite du destructeur
    //
    delete PtrDuree;       // libération de la mémoire

} // Alloc()

    La présence de l'instruction delete en fin de fonction permet la libération de la mémoire. Cependant, le destructeur est appelé pour la seconde fois. Cela peut n'avoir aucune conséquence fâcheuse, par exemple s'il s'agit d'un destructeur "trivial". En revanche, cela peut dans certains cas générer des erreurs graves, par exemple si le destructeur ferme un fichier, s'il inverse une bascule, s'il modifie l'état d'un sémaphore, etc.

    Cette technique est cependant parfois extrèmement efficace, si une fonction doit utiliser successivement plusieurs objets de même type. Au lieu d''allouer et de desallouer successivement chacun d'eux (l'appel à l'allocateur mémoire est une opération qui peut être très coûteuse en temps machine), il suffit d'allouer le premier, puis de le détruire, et de construire les autres à la place du premier, seul le dernier étant détruit par l'instruction delete. Cette posibilité est décrite dans le paragraphe : Opérateurs new/delete avec placement

Surcharge des opérateurs globaux

    Les opérateurs globaux new et delete peuvent être surchargés en utilisant les profils indiqués plus haut. Ceci ne doit être fait qu'avec la plus extrême prudence, et dans des circonstances très particulières. Il s'agit de remplacer l'allocation/désallocation standard de la mémoire dynamique, fournie par défaut par tout compilateur C++, par sa propre gestion de la mémoire. Il faut bien noter que toutes les opérations d'allocation/désallocation du programme, qu'elles soient explicites ou implicites, passeront par ces nouveaux opérateurs. Garreta [3] signale en particulier que la surcharge de l'un quelconque de ces opérateurs implique que les versions par défaut de tous les autres opérateurs new/delete ne sont plus accessibles. Ceci peut être simplement vérifié par l'exemple suivant [4] :
 
#include <stdlib.h>      // malloc()

void* operator new (std::size_t size)
{
    cout << "Taille  " << size << endl;
    return malloc (size);

} // new()

// ...
::new int;         // Taille = 4
::new short [10];  // Taille = 20 : tableau de 10 éléments

    malloc(), déclarée dans le fichier <stdlib.h>, est une fonction standard du C qui alloue en mémoire dynamique le nombre d'octets qui lui est passé en paramètre, et renvoie l'adresse de la zone mémoire correctement alignée pour tout type d'objet. Comme on le constate, la surcharge de l'opérateur remplace les opérateurs new et new[].

    Rappelons cependant que, dans certains domaines, en particulier la programmation d'applications "temps réel", l'utilisation de mémoire dynamique est interdite, quel que soit le langage (même ADA 95 !). Cela est dû au fait que le temps de recherche et d'allocation de la mémoire dynamique est imprévisible, et peut même échouer. Une alternative est de substituer aux opérateurs new/delete standard ses propres opérateurs qui "puisent" dans un espace mémoire alloué une fois pour toutes en début de programme, et dont la taille est suffisante pour toute la durée du programme. Cela permet de ne pas nécessiter de profondes modifications du code. On peut aussi imaginer utiliser cette technique pour des applications qui devraient être très rapides, par exemple des serveurs. Il faut cependant noter que la substitution des fonctions standard par les fonctions personnelles est effectuée avant le début de l'application et valable pour toute la durée de l'exécution. Il est donc impossible de restaurer les traitements par défaut en cours d'exécution.

Surcharge des opérateurs dans une classe

    Les opérateurs globaux new et delete peuvent être surchargés dans une classe. Contrairement au cas précédent, cette opération est beaucoup plus intéressante, pour les raisons suivantes :

    Il est très facile de les implémenter, comme le montre l'exemple ci-dessus :
 
#include <stdlib.h> // malloc()

class CX
{
    char m_Char;

  public :
    CX  (void) { cout << "Constr." << endl; }
    ~CX (void) { cout << "Destr."  << endl; }

    void * operator new (size_t Size) throw (bad_alloc) 
    { 
        cout << "Mon new()" << endl;
        return ::malloc (Size);  
        // ou 
        // return ::new char [Size];
    }

}; // CX
// ...
CX * Ptr    = new CX;
CX * PtrTab = new CX [2];

qui provoque l'affichage :
 
Mon new()
Constr.
Constr.
Constr.
Destr.
Destr.
Destr.

    Sur cet exemple, on constate que :

    Notons cependant une erreur à ne pas commettre, celle de réutiliser l'opérateur global ::new pour créer l'objet CX :
 
class CX
{
    // ...

    void * operator new (size_t Size) throw (bad_alloc) 
    { 
        void * Ptr = ::new CX;
        cout << "Adresse renvoyée par ::new() : " << Ptr << endl;
        return Ptr;

     } // new()

}; // CX

CX * Ptr = new CX;
cout << "Adresse de l'objet CX : " << Ptr << endl;

dont l'exécution provoque :
 
Constr.
Adresse renvoyée par ::new() : 0x8061798
Constr.
Adresse de l'objet CX : 0x8061798

    Comme on le voit, le constructeur est appelé deux fois, la première par l'opérateur global ::new, l'autre en fin de la surcharge de new. Il n'y a cependant qu'un seul objet créé, ce qui peut être vérifié par les adresses mémoire. Cela peut avoir des conséquences catastrophiques si le constructeur a des effets de bord.

Opérateurs new[]/delete[] pour des tableaux

Allocation de tableaux d'objets en mémoire dynamique

    Il est possible d'allouer des tableaux d'éléments en mémoire dynamique grâce aux opérateurs new[]/delete[] :
 
void Alloc (void) 
 { 
    int    * PtrInt   = new int    [10]; 
    char   * PtrChar  = new char   [20]; 
    CDuree * PtrDuree = new CDuree [30]; 
    // ... 
    delete [] PtrInt;
    delete [] PtrChar;
    delete [] PtrDuree;

 } // Alloc() 

    PtrInt pointe sur un tableau de 10 entiers. De même, PtrChar pointe sur un tableau de 20 caractères. Enfin, PtrDuree pointe sur un tableau de 30 objets de la classe CDuree (supposée exister). Dans ce dernier cas, l'opérateur new appelle 30 fois le constructeur par défaut de la classe CDuree pour construire les éléments du tableau dans l'ordre des indices croissants (à prendre en compte s'il y a des effets de bord possibles). Comme précédemment, si la classe ne possède pas un tel constructeur, cela provoque une erreur de compilation.

    Contrairement au cas précédent, il n'est pas possible d'initialiser les objets avec un autre constructeur que le constructeur par défaut.

    La libération de la mémoire doit impérativement être effectuée au moyen de l'opérateur delete [] (l'oubli des [] rend le comportement du programme imprévisible !). Les objets sont détruits dans l'ordre des indices décroissants.

Accès aux éléments du tableau

    L'accès à un élément peut se faire par l'une des deux écritures suivantes, la seconde étant beaucoup plus lisible, donc en pratique la seule conseillée :
 
int i = (*PtrInt) + 4; // accède à l'élément d'indice 4 (le cinquième)
int j = PtrInt [4];    // idem

Surcharge des opérateurs globaux

    Comme précédemment, il est possible de surcharger les opérateurs new[]/delete[] globaux pour une classe donnée, en utilisant les profils indiqués plus haut. Il est malheureusement impossible de pouvoir agir sur les constructeurs utilisés, car, comme nous l'avons signalé plus haut, ils sont appelés après le retour de l'opérateur surchargé :
 

class CX
{
    // ...
    void * operator new [] (size_t Size) throw (bad_alloc)
    {
        cout << "Size = " << Size << endl;
        return ::new char [Size];

    } // new []

    void operator delete [] (void * p) throw ()
    {
        cout << "Adresse : " << p << endl;
        ::delete [] p;

    } // delete[]
    }

}; // CX
// ...
cout << "sizeof (CX) = " << sizeof (CX) << endl;
CX * PtrTab = new CX [2];
delete [] PtrTab;

    L'affichage obtenu est le suivant :
 
sizeof (CX) = 1
Size = 6
Adresse : 0x8061770

    On constate que la taille de la mémoire à allouer est directement calculée par le compilateur. La norme C++ indique qu'elle doit être supérieure ou égale à l'espace minimal nécessaire (N * sizeof (CX)), où N est ne nombre d'éléments du tableau. On constate ici qu'elle est supérieure de 4 octets. Cela dépend de l'implémentation. En fait, la valeur de N est elle aussi stockée dans la mémoire dynamique, pour que l'espace puisse être correctement libéré. Il apparaît en effet que l'opérateur delete[] ne reçoit comme paramètre que l'adresse de l'espace à libérer, et non sa taille. Celle-ci doit donc être accessible par ailleurs.

Opérateurs new/delete avec placement

    Les opérateurs new/delete ou new[]/delete[] avec placement permettent de ne pas utiliser ou allouer la mémoire dynamique, mais de placer le ou les objets à une adresse prédéfinie. Cette opération peut être intéressante dans des cas très particuliers, mais doit être faite avec beaucoup de prudence, afin de ne pas provoquer d'erreurs de violation de mémoire.

    Dans l'exemple ci-dessous, on suppose que les deux tableaux de doubles et de shorts sont utilisés successivement dans une fonction. Afin d'économiser la pile d'exécution, et d'éviter des allocations/libérations en mémoire dynamique, on alloue le second tableau au même endroit de la pile :
 
double TabDouble [1000];

// utilisation de TabDouble

short * TabShort = new (TabDouble) short [1000];

// Plus d'utilisation de TabDouble et utilisation de TabShort

 cout <<   "Adr. de TabDouble = " << TabDouble
      << "\nAdr. de TabShort  = " << TabShort << endl;

    qui provoque l'affichage suivant :
 
Adr. de TabDouble = 0x7fffda08
Adr. de PtrShort  = 0x7fffda08

    Cet exemple illustre seulement comment cela peut être réalisé, mais pas du tout de ce qu'il faut réaliser ! Noter que l'utilisation de delete[] est totalement inutile ici.

    Lorsqu'il s'agit d'objets, l'opérateur new avec placement appelle le constructeur, l'opérateur delete avec placement doit être appelé : il ne libère aucun espace mémoire, mais appelle le destructeur.

    L'exemple ci-dessous illustre la superposition d'objets successifs dans le même espace de mémoire dynamique, alloué et libéré une seule fois :
 
void Alloc (void)
{
    CDuree * PtrDuree = new CDuree (3600);
    // ...
    PtrDuree->~CDuree();           // appel explicite du destructeur
    new (PtrDuree) CDuree (1300);  // construction d'un nouvel objet à la place de l'ancien 
    // ...
    PtrDuree->~CDuree();           // appel explicite du destructeur
    new (PtrDuree) CDuree ( 500);  // construction d'un nouvel objet à la place de l'ancien 
    // ... 
    delete PtrDuree;               // destruction du dernier objet et libération de la mémoire

} // Alloc()

    L'opérateur new avec placement est indispensable pour placer un objet en mémoire partagée (voir cours système de deuxième année).

    Comme précédemment, il est possible de surcharger les opérateurs new/delete globaux pour une classe donnée, en utilisant les profils indiqués plus haut.

Exceptions et traitement d'erreur

    Par défaut, l'opérateur new, dans toutes les versions étudiées ci-dessus (sauf l'opérateur avec placement bien entendu), lève une exception de la classe bad_alloc en cas d'échec (mémoire dynamique insuffisante) [2]. Dans de nombreux compilateurs non encore conformes à la norme, l'opérateur new renvoie un pointeur nul en cas d'échec. Ce comportement peut être obtenu en ajoutant à toutes les versions de new ci-dessus un paramètre supplémentaire ayant pour valeur la constante std::nothrow :
 
void Alloc (void)
{
    for (CDuree * Ptr; Ptr = new (std::nothrow) CDuree; ) { /* ... */ }

    // Fin de boucle quand la mémoire est insuffisante

} // Alloc()


[1] Rappelons qu'un constructeur par défaut est un constructeur qui peut être appelé sans paramètre, soit parce qu'il n'a effectivement pas de paramètre, soit parce que les paramètres formels ont tous une valeur par défaut.

[2] Dans la version actuelle du compilateur (gcc version 2.95.3 20010315 (release)), cette exception n'est pas levée et le programme plante sur une erreur système "Segmentation fault" ...

[3] Henri Garreta, "Le langage et la bibliothèque C++", Ed. Ellipses, Paris, 2000

[4] gcc version 2.95.3 20010315 (release)