new
et delete
Nous avons dit au chapitre 3 que new
et delete
étaient des opérateurs unaires. Cependant, étant donné leur usage un peu particulier, ils possèdent leurs règles propres pour la redéfinition.
Commençons par expliquer à quoi peut servir la redéfinition de tels opérateurs. Imaginons un programme dans lequel on souhaite gérer des structures dynamiques, du type liste chaînée, arbre, etc., contenant de nombreux pointeurs sur des éléments nombreux mais de petite taille (par exemple des entiers int
). La gestion de la mémoire va alors poser problème, car lallocateur standard de mémoire malloc
est peu adapté aux petits blocs : en effet, il conserve plusieurs informations sur chaque bloc, comme sa taille, etc., qui ne sont pas forcément utiles et surtout prennent beaucoup de place par comparaison à la taille dun entier. Il est facile de le vérifier, en utilisant la fonction coreleft()
qui indique la place mémoire disponible. En faisant 1 000 appels à malloc(2)
, celle-ci diminue de 8 000 (et non 2 000).
Lidée est alors de ranger tous ces petits entiers dans une même table, afin quils soient compactés au maximum. Une table de bits auxiliaire indiquera simplement lemplacement des éléments libres de la table. Pour cela, nous allons redéfinir les opérateurs new
et delete
sur un type element
formellement identique à un entier. Pour ne pas avoir à réécrire tous les opérateurs sur les entiers, nous définissons aussi un opérateur de changement de type de element
vers int
et un constructeur de int
vers element
. Voici la classe obtenue :
#include <alloc.h> #include <mem.h> class element { int i; static element* table; // table d'alloc. mémoire static char* libres; // indicat. de blocs libres public : static unsigned tailletable; // taille de table element() { i = 0; } element(int j) { i = j; } operator int() { return i; } void* operator new(unsigned); void operator delete(void *, unsigned); }; void* element::operator new(unsigned taille){ if (taille != sizeof(element)) return malloc(taille); if (!table) { // table à allouer unsigned max = 65535/(8*taille+1); // taille max. if (!tailletable) tailletable = 100; if (tailletable > max) tailletable = max; table = (element*) calloc(tailletable, 8*taille+1); if (!table) return 0; libres = (char*)(table + 8*tailletable); memset(libres, ~0, tailletable); } unsigned numero // chercher le premier bloc libre char *tablefin = libres + tailletable; for (char *p = libres; (*p == 0) && (p < tablefin); p++); if (p >= tablefin) return ;0 // table pleine numero = 8*(p -libres); unsigned char octet = *p, masque = 1; while ( (octet & masque) == 0) { masque <<= 1; numero++; } *p -= masque; // mettre bit à 0 : occupé return table + numero;} void element::operator delete(void* pe, unsigned taille){ if (!pe) return; // pe est nul if (taille != sizeof(element)) // pas alloué dans table delete pe; return; } unsigned numero = (element*)pe -table; unsigned char masque = 1 << (numero%8); char *p = libres + numero/8; *p |= masque; // mettre bit à 1 pour libérer} main(){ element *tab[10]; for (int i = 0; i < 10; i++) tab[i] = new element(i+10); // 10 allocations *tab[1] = *tab[5] + (*tab[6])/(*tab[0]); // par exemple for (i = 0; i < 10; i++) delete tab[i]; // autant de libérations return 0;}
Noter la syntaxe de déclaration des deux fonctions opérateurs. Dans le cas de new
, il sagit dune fonction renvoyant un pointeur void*
(adresse de lélément alloué), et ayant au moins un paramètre de type unsigned
(ou size_t
ce qui est équivalent, ce dernier type étant défini dans <alloc.h>
) qui indique la taille de lélément à allouer. On pourrait penser ici que cette taille est forcément égale à 2 (taille de element
), mais il nen est rien car, comme on le verra au chapitre 8, des classes peuvent avoir hérité de element
(avec lopérateur new
associé), et être plus grandes. Dans ce cas, on ne peut les mettre dans notre table, cest pourquoi lorsque taille
est plus grand que sizeof(element)
, lopérateur renvoie un bloc normal de la mémoire dynamique dans notre exemple.
Lopérateur delete
est une fonction sans résultat, avec un paramètre pointeur void*
(lélément à supprimer) et un second paramètre optionnel indiquant lui aussi la taille.
La syntaxe de ces fonctions est particulière à plus dun titre. Dabord, bien quil sagisse ici de fonctions membres, il nest pas permis de faire référence à des champs non statiques, ni à this
. En effet, ces fonctions sont appelées pour des objets en cours de création, avant le constructeur, ou en cours de destruction, après le destructeur. Précisons cependant que les deux fonctions connaissent ladresse correspondant à this
: dans le cas de delete
, cest le premier argument, dans le cas de new
, cest la valeur à calculer. Par conséquent, lopérateur new
par exemple peut placer des valeurs dans ce qui deviendra lobjet, et notamment des zéros.
Dautre part, la syntaxe dappel est assez curieuse, comme on le sait déjà, puisquelle reste identique à celle de lopérateur prédéfini. Noter en particulier que bien que new
renvoie un pointeur void*
, cest un element*
qui est reçu en fait par tab[i]
dans notre exemple, ainsi quon peut légitimement lespérer.
Donnons quelques explications sur notre programme exemple, qui est très typique de ce genre de manipulation. On a placé en membre statique de element
un pointeur table
qui donne le début de la table où sont placés les éléments, et un pointeur doctets libres
, qui désigne une liste de bits mis à 1 si lemplacement correspondant est libre, à 0 sinon. Ces deux membres sont privés et ne sont utilisés que par new
et delete
. Le troisième membre statique est un entier qui indique la taille de la table à allouer, divisée par huit. La valeur par défaut est 100 (ce qui permet de placer 800 entiers dans la table), mais on peut la changer avant le premier appel à new
(qui initialise la table) afin davoir plus ou moins de place. En effet, quand la table est pleine, new
renvoie zéro ; il ne peut pas augmenter la taille de la table, car un appel de realloc
changerait ladresse de celle-ci, et par conséquent ferait perdre toutes les données de la table pointées par des pointeurs extérieurs (comme tab[i]
dans main
).
Chaque élément va occuper 17 bits dans la table : seize à un emplacement pointé par le résultat de new
, plus un dix-septième au même emplacement relatif par rapport au début de la table de bits pointés par liste
. La taille totale occupée par lensemble des tables est donc (8*2+1)*tailletable
, puisquil y a 8*tailletable
éléments. Lorsque new
trouve une valeur de table nulle au départ (table non encore allouée), il alloue un bloc de cette dimension en mémoire par un appel à calloc
(variante de malloc
). La table proprement dite occupe les 8*2*tailletable
premiers octets, tandis que la table de bits pointée par libres
occupe les tailletable
octets restants.
Pour calculer son résultat, new
cherche le premier bit à 1 dans la table de bits ; pour cela il cherche le premier octet non nul, et le décale autant de fois que nécessaire pour obtenir un bit à 1 ; on utilise pour cela un masque du type 0..010..0
(binaire). On met le bit à 0 en soustrayant le masque, et lon renvoie table + numero
, où numero
est lindex de la position nouvellement prise dans la table.
Lopérateur delete
se contente quant à lui de mettre à 1 le même bit à laide dun « ou » logique (il ne faut pas employer une addition, car le bit peut être déjà à 1 : dans notre exemple, il ny a aucune erreur si lon désalloue deux fois le même bloc).
Ce système est très utile et fait gagner beaucoup de place, puisque chaque entier occupe à présent 17 bits de mémoire, contre 64 avec lallocateur standard. Lallocation de 1 000 éléments occupe au total 2 125 octets.
Noter que de tels opérateurs ne sappliquent pas sur les tableaux. Ainsi, si lon écrit :
element *tab = new element[5];
cest lopérateur standard qui est utilisé, puisquon ne peut redéfinir les opérateurs sur les tableaux. Par contre, rien nempêche décrire une classe simulant un tableau avec un opérateur new
pour avoir le même effet.
Lopérateur new
peut avoir des arguments supplémentaires. Dans ce cas, ceux-ci doivent être écrits entre parenthèses derrière le mot new
lors de son appel. Voici un exemple :
class exemple { // ... void* operator new(unsigned, void* adresse = 0) { if (adresse) return adresse; else return malloc(taille); } void operator delete(void *p) { free(p); } } // ..... exemple *exp = new exemple;char tampon[sizeof(exemple)];exemple *exp2 = new(&tampon) exemple;
Ici nous avons ajouté un second paramètre adresse
, qui indique une adresse où stocker lobjet. Ainsi, si le pointeur exp
pointe sur un bloc normal de la mémoire, puisque dans ce cas on na pas précisé adresse
(qui a donc la valeur par défaut 0, doù appel de malloc
), par contre exp2
pointe sur le tableau tampon
. Quel est lintérêt dune telle manoeuvre ? Elle permet dappeler un constructeur pour un objet placé dans le tableau tampon
, ce qui nest pas possible autrement. Quel en est le danger ? Cest que le programmeur appelle delete
avec exp2
, alors quaucun bloc na été alloué. Il faut ici appeler explicitement le destructeur, comme on la dit au chapitre 6 :
exp2->exemple::~exemple();
et non delete
. Une méthode plus sûre consisterait à placer un champ dans la classe indiquant si lobjet a été alloué en mémoire dynamique ou dans un tampon ; ce champ peut être retrouvé par delete
qui connaît ladresse de lobjet à détruire.
Il nest pas possible de passer un paramètre supplémentaire à delete
(Error : 'operator delete' must be declared with one argument, lopérateur delete doit être déclaré avec un argument, ce qui est dailleurs faux puisquon peut en placer un second pour la taille).
Précédent | Sommaire | Suivant |