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 :
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.
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é.
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).
cout << "L'entier
= " << *PtrInt << endl;
cout << "Le caractère = " << *PtrChar << endl; cout << "La durée = " << *PtrDuree << endl; // si la durée est affichable |
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
|
Attention : l'opérateur delete ne modifie pas la valeur du pointeur. Il appartient donc à l'utilisateur de veiller à ne plus l'utiliser.
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
#include <stdlib.h> //
malloc()
void* operator new (std::size_t size)
} // new() // ...
|
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.
Il est très facile de les implémenter,
comme le montre l'exemple ci-dessus :
#include <stdlib.h> // malloc()
class CX
public :
void * operator new (size_t Size) throw (bad_alloc)
}; // CX
|
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)
} // new() }; // CX CX * Ptr = new CX;
|
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.
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.
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 |
class CX
} // new [] void operator delete [] (void * p) throw ()
} // delete[]
}; // CX
|
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.
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
|
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.
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