Opérateurs 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 l’allocateur 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 d’un 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).

L’idée est alors de ranger tous ces petits entiers dans une même table, afin qu’ils soient compactés au maximum. Une table de bits auxiliaire indiquera simplement l’emplacement 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 s’agit d’une 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 n’en est rien car, comme on le verra au chapitre 8, des classes peuvent avoir hérité de element (avec l’opérateur new associé), et être plus grandes. Dans ce cas, on ne peut les mettre dans notre table, c’est pourquoi lorsque taille est plus grand que sizeof(element), l’opérateur renvoie un bloc normal de la mémoire dynamique dans notre exemple.

L’opé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 d’un titre. D’abord, bien qu’il s’agisse ici de fonctions membres, il n’est 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 l’adresse correspondant à this : dans le cas de delete, c’est le premier argument, dans le cas de new, c’est la valeur à calculer. Par conséquent, l’opérateur new par exemple peut placer des valeurs dans ce qui deviendra l’objet, et notamment des zéros.

D’autre part, la syntaxe d’appel est assez curieuse, comme on le sait déjà, puisqu’elle reste identique à celle de l’opérateur prédéfini. Noter en particulier que bien que new renvoie un pointeur void*, c’est un element* qui est reçu en fait par tab[i] dans notre exemple, ainsi qu’on peut légitimement l’espé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 d’octets libres, qui désigne une liste de bits mis à 1 si l’emplacement 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 d’avoir 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 l’adresse 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 l’ensemble des tables est donc (8*2+1)*tailletable, puisqu’il 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 l’on renvoie table + numero, où numero est l’index de la position nouvellement prise dans la table.

L’opérateur delete se contente quant à lui de mettre à 1 le même bit à l’aide d’un « ou » logique (il ne faut pas employer une addition, car le bit peut être déjà à 1 : dans notre exemple, il n’y a aucune erreur si l’on 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 l’allocateur standard. L’allocation de 1 000 éléments occupe au total 2 125 octets.

Noter que de tels opérateurs ne s’appliquent pas sur les tableaux. Ainsi, si l’on écrit :

element *tab = new element[5];

c’est l’opérateur standard qui est utilisé, puisqu’on ne peut redéfinir les opérateurs sur les tableaux. Par contre, rien n’empêche d’écrire une classe simulant un tableau avec un opérateur new pour avoir le même effet.

L’opé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 l’objet. Ainsi, si le pointeur exp pointe sur un bloc normal de la mémoire, puisque dans ce cas on n’a pas précisé adresse (qui a donc la valeur par défaut 0, d’où appel de malloc), par contre exp2 pointe sur le tableau tampon. Quel est l’intérêt d’une telle manoeuvre ? Elle permet d’appeler un constructeur pour un objet placé dans le tableau tampon, ce qui n’est pas possible autrement. Quel en est le danger ? C’est que le programmeur appelle delete avec exp2, alors qu’aucun bloc n’a été alloué. Il faut ici appeler explicitement le destructeur, comme on l’a 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 l’objet a été alloué en mémoire dynamique ou dans un tampon ; ce champ peut être retrouvé par delete qui connaît l’adresse de l’objet à détruire.

Il n’est pas possible de passer un paramètre supplémentaire à delete (Error : 'operator delete' must be declared with one argument, l’opérateur delete doit être déclaré avec un argument, ce qui est d’ailleurs faux puisqu’on peut en placer un second pour la taille).

Précédent Précédent Sommaire Sommaire Suivant Suivant