• Aucun résultat trouvé

Exemple 7-2. Opérateur de résolution de portée int valeur(void) // Fonction globale

7.11. Surcharge des opérateurs

7.11.9. Opérateurs d’allocation dynamique de mémoire

int i; // Donnée à accéder.

};

Encapsulee o; // Objet à manipuler.

// Cette classe est la classe encapsulante : struct Encapsulante

e->i=2; // Enregistre 2 dans o.i.

(*e).i = 3; // Enregistre 3 dans o.i.

Encapsulee *p = &e;

p->i = 4; // Enregistre 4 dans o.i.

return ; }

7.11.9. Opérateurs d’allocation dynamique de mémoire

Les opérateurs les plus difficiles à écrire sont sans doute les opérateurs d’allocation dynamique de mémoire. Ces opérateurs prennent un nombre variable de paramètres, parce qu’ils sont complètement surchargeables (c’est à dire qu’il est possible de définir plusieurs surcharges de ces opérateurs même au sein d’une même classe, s’ils sont définis de manière interne). Il est donc possible de définir plusieurs opérateurs newounew[], et plusieurs opérateursdeleteoudelete[]. Cependant, les premiers paramètres de ces opérateurs doivent toujours être la taille de la zone de la mémoire à allouer dans le cas des opérateursnewetnew[], et le pointeur sur la zone de la mémoire à restituer dans le cas des opérateursdeleteetdelete[].

La forme la plus simple de newne prend qu’un paramètre : le nombre d’octets à allouer, qui vaut toujours la taille de l’objet à construire. Il doit renvoyer un pointeur du type void. L’opérateurdelete correspondant peut prendre, quant à lui, soit un, soit deux paramètres. Comme on l’a déjà dit, le premier paramètre est toujours un pointeur du type void sur l’objet à détruire. Le deuxième paramètre,

Chapitre 7. C++ : la couche objet

s’il existe, est du type size_t et contient la taille de l’objet à détruire. Les mêmes règles s’appliquent pour les opérateursnew[]etdelete[], utilisés pour les tableaux.

Lorsque les opérateursdeleteetdelete[]prennent deux paramètres, le deuxième paramètre est la taille de la zone de la mémoire à restituer. Cela signifie que le compilateur se charge de mémoriser cette information. Pour les opérateursnewetdelete, cela ne cause pas de problème, puisque la taille de cette zone est fixée par le type de l’objet. En revanche, pour les tableaux, la taille du tableau doit être stockée avec le tableau. En général, le compilateur utilise un en-tête devant le tableau d’objets.

C’est pour cela que la taille à allouer passée ànew[], qui est la même que la taille à désallouer passée en paramètre àdelete[], n’est pas égale à la taille d’un objet multipliée par le nombre d’objets du tableau. Le compilateur demande un peu plus de mémoire, pour mémoriser la taille du tableau. On ne peut donc pas, dans ce cas, faire d’hypothèses quant à la structure que le compilateur donnera à la mémoire allouée pour stocker le tableau.

En revanche, sidelete[]ne prend en paramètre que le pointeur sur le tableau, la mémorisation de la taille du tableau est à la charge du programmeur. Dans ce cas, le compilateur donne ànew[]la valeur exacte de la taille du tableau, à savoir la taille d’un objet multipliée par le nombre d’objets dans le tableau.

Exemple 7-21. Détermination de la taille de l’en-tête des tableaux

#include <stdlib.h>

#include <stdio.h>

int buffer[256]; // Buffer servant à stocker le tableau.

class Temp {

char i[13]; // sizeof(Temp) doit être premier.

public:

static void *operator new[](size_t taille) {

return buffer;

}

static void operator delete[](void *p, size_t taille) {

printf("Taille de l’en-tête : %d\n",

taille-(taille/sizeof(Temp))*sizeof(Temp));

Il est à noter qu’aucun des opérateursnew,delete,new[]etdelete[]ne reçoit le pointeurthis en paramètre : ce sont des opérateurs statiques. Cela est normal puisque, lorsqu’ils s’exécutent, soit l’objet n’est pas encore créé, soit il est déjà détruit. Le pointeurthisn’existe donc pas encore (ou n’est plus valide) lors de l’appel de ces opérateurs.

Les opérateursnewetnew[]peuvent avoir une forme encore un peu plus compliquée, qui permet de leur passer des paramètres lors de l’allocation de la mémoire. Les paramètres supplémentaires doivent

Chapitre 7. C++ : la couche objet impérativement être les paramètres deux et suivants, puisque le premier paramètre indique toujours la taille de la zone de mémoire à allouer.

Comme le premier paramètre est calculé par le compilateur, il n’y a pas de syntaxe permettant de le passer aux opérateursnewetnew[]. En revanche, une syntaxe spéciale est nécessaire pour passer les paramètres supplémentaires. Cette syntaxe est détaillée ci-dessous.

Si l’opérateurnewest déclaré de la manière suivante dans la classe classe : static void *operator new(size_t taille, paramètres);

où taille est la taille de la zone de mémoire à allouer etparamètres la liste des paramètres additionnels, alors on doit l’appeler avec la syntaxe suivante :

new(paramètres) classe;

Les paramètres sont donc passés entre parenthèses comme pour une fonction normale. Le nom de la fonction estnew, et le nom de la classe suit l’expressionnewcomme dans la syntaxe sans paramètres.

Cette utilisation denewest appeléenew avec placement.

Le placement est souvent utilisé afin de réaliser des réallocations de mémoire d’un objet à un autre.

Par exemple, si l’on doit détruire un objet alloué dynamiquement et en reconstruire immédiatement un autre du même type, les opérations suivantes se déroulent :

1. appel du destructeur de l’objet (réalisé par l’expressiondelete) ; 2. appel de l’opérateurdelete;

3. appel de l’opérateurnew;

4. appel du constructeur du nouvel objet (réalisé par l’expressionnew).

Cela n’est pas très efficace, puisque la mémoire est restituée pour être allouée de nouveau immédiate-ment après. Il est beaucoup plus logique de réutiliser la mémoire de l’objet à détruire pour le nouvel objet, et de reconstruire ce dernier dans cette mémoire. Cela peut se faire comme suit :

1. appel explicite du destructeur de l’objet à détruire ;

2. appel denewavec comme paramètre supplémentaire le pointeur sur l’objet détruit ; 3. appel du constructeur du deuxième objet (réalisé par l’expressionnew).

L’appel denewne fait alors aucune allocation : on gagne ainsi beaucoup de temps.

Exemple 7-22. Opérateurs new avec placement

#include <stdlib.h>

class A { public:

A(void) // Constructeur.

{

return ; }

Chapitre 7. C++ : la couche objet

~A(void) // Destructeur.

{

return ; }

// L’opérateur new suivant utilise le placement.

// Il reçoit en paramètre le pointeur sur le bloc // à utiliser pour la requête d’allocation dynamique // de mémoire.

static void *operator new (size_t taille, A *bloc) {

return (void *) bloc;

}

// Opérateur new normal :

static void *operator new(size_t taille) {

// Implémentation : return malloc(taille);

}

// Opérateur delete normal :

static void operator delete(void *pBlock) {

A *pA=new A; // Création d’un objet de classe A.

// L’opérateur new global du C++ est utilisé.

pA->~A(); // Appel explicite du destructeur de A.

A *pB=new(pA) A; // Réutilisation de la mémoire de A.

delete pB; // Destruction de l’objet.

return EXIT_SUCCESS;

}

Dans cet exemple, la gestion de la mémoire est réalisée par les opérateursnewetdeletenormaux.

Cependant, la réutilisation de la mémoire allouée se fait grâce à un opérateurnewavec placement, défini pour l’occasion. Ce dernier ne fait strictement rien d’autre que de renvoyer le pointeur qu’on lui a passé en paramètre. On notera qu’il est nécessaire d’appeler explicitement le destructeur de la classe A avant de réutiliser la mémoire de l’objet, car aucune expression delete ne s’en charge avant la réutilisation de la mémoire.

Note :Les opérateursnewetdeleteavec placement prédéfinis par la bibliothèque standard C++

effectuent exactement ce que les opérateurs de cet exemple font. Il n’est donc pas nécessaire de les définir, si on ne fait aucun autre traitement que de réutiliser le bloc mémoire que l’opérateur newreçoit en paramètre.

Il est impossible de passer des paramètres à l’opérateurdeletedans une expressiondelete. Cela est dû au fait qu’en général on ne connaît pas le contexte de la destruction d’un objet (alors qu’à l’allocation, on connaît le contexte de création de l’objet). Normalement, il ne peut donc y avoir

Chapitre 7. C++ : la couche objet qu’un seul opérateurdelete. Cependant, il existe un cas où l’on connaît le contexte de l’appel de l’opérateurdelete : c’est le cas où le constructeur de la classe lance une exception (voir le Cha-pitre 8 pour plus de détails à ce sujet). Dans ce cas, la mémoire allouée par l’opérateur new doit être restituée et l’opérateur delete est automatiquement appelé, puisque l’objet n’a pas pu être construit. Afin d’obtenir un comportement symétrique, il est permis de donner des paramètres ad-ditionnels à l’opérateur delete. Lorsqu’une exception est lancée dans le constructeur de l’objet alloué, l’opérateur delete appelé est l’opérateur dont la liste des paramètres correspond à celle de l’opérateurnewqui a été utilisé pour créer l’objet. Les paramètres passés à l’opérateurdelete prennent alors exactement les mêmes valeurs que celles qui ont été données aux paramètres de l’opérateurnew lors de l’allocation de la mémoire de l’objet. Ainsi, si l’opérateurnewa été utili-sé sans placement, l’opérateurdeletesans placement sera appelé. En revanche, si l’opérateurnewa été appelé avec des paramètres, l’opérateurdeletequi a les mêmes paramètres sera appelé. Si aucun opérateurdelete ne correspond, aucun opérateurdeleten’est appelé (si l’opérateurnewn’a pas alloué de mémoire, cela n’est pas grave, en revanche, si de la mémoire a été allouée, elle ne sera pas restituée). Il est donc important de définir un opérateurdeleteavec placement pour chaque opérateur newavec placement défini. L’exemple précédent doit donc être réécrit de la manière suivante :

#include <stdlib.h>

static bool bThrow = false;

class A { public:

A(void) // Constructeur.

{

// Le constructeur est susceptible // de lancer une exception : if (bThrow) throw 2;

// L’opérateur new suivant utilise le placement.

// Il reçoit en paramètre le pointeur sur le bloc // à utiliser pour la requête d’allocation dynamique // de mémoire.

static void *operator new (size_t taille, A *bloc) {

return (void *) bloc;

}

// L’opérateur delete suivant est utilisé dans les expressions // qui utilisent l’opérateur new avec placement ci-dessus, // si une exception se produit dans le constructeur.

static void operator delete(void *p, A *bloc) {

// On ne fait rien, parce que l’opérateur new correspondant // n’a pas alloué de mémoire.

return ; }

Chapitre 7. C++ : la couche objet

// Opérateur new et delete normaux : static void *operator new(size_t taille) {

return malloc(taille);

}

static void operator delete(void *pBlock) {

A *pA=new A; // Création d’un objet de classe A.

pA->~A(); // Appel explicite du destructeur de A.

bThrow = true; // Maintenant, le constructeur de A lance // une exception.

try {

A *pB=new(pA) A; // Réutilisation de la mémoire de A.

// Si une exception a lieu, l’opérateur // delete(void *, A *) avec placement // est utilisé.

delete pB; // Destruction de l’objet.

}

catch (...) {

// L’opérateur delete(void *, A *) ne libère pas la mémoire // allouée lors du premier new. Il faut donc quand même // le faire, mais sans delete, car l’objet pointé par pA // est déjà détruit, et celui pointé par pB l’a été par

Note :Il est possible d’utiliser le placement avec les opérateursnew[]etdelete[]exactement de la même manière qu’avec les opérateursnewetdelete.

On notera que lorsque l’opérateur new est utilisé avec placement, si le deuxième argument est de type size_t, l’opérateur deleteà deux arguments peut être interprété soit comme un opérateurdeleteclassique sans placement mais avec deux paramètres, soit comme l’opérateur deleteavec placement correspondant à l’opérateurnewavec placement. Afin de résoudre cette ambiguïté, le compilateur interprète systématiquement l’opérateur delete avec un deuxième paramètre de type size_t comme étant l’opérateur à deux paramètres sans placement. Il est donc impossible de définir un opérateurdeleteavec placement s’il a deux paramètres, le deuxième étant de type size_t. Il en est de même avec les opérateursnew[]etdelete[].

Quelle que soit la syntaxe que vous désirez utiliser, les opérateursnew,new[],deleteetdelete[]

doivent avoir un comportement bien déterminé. En particulier, les opérateursdeleteetdelete[]

Chapitre 7. C++ : la couche objet doivent pouvoir accepter un pointeur nul en paramètre. Lorsqu’un tel pointeur est utilisé dans une expressiondelete, aucun traitement ne doit être fait.

Enfin, vos opérateursnewetnew[]doivent, en cas de manque de mémoire, appeler un gestionnaire d’erreur. Le gestionnaire d’erreur fourni par défaut lance une exception de classe std::bad_alloc (voir le Chapitre 8 pour plus de détails sur les exceptions). Cette classe est définie comme suit dans le fichier d’en-têtenew:

class bad_alloc : public exception {

public:

bad_alloc(void) throw();

bad_alloc(const bad_alloc &) throw();

bad_alloc &operator=(const bad_alloc &) throw();

virtual ~bad_alloc(void) throw();

virtual const char *what(void) const throw();

};

Note :Comme son nom l’indique, cette classe est définie dans l’espace de nommagestd::. Si vous ne voulez pas utiliser les notions des espaces de nommage, vous devrez inclure le fichier d’en-têtenew.hau lieu denew. Vous obtiendrez de plus amples renseignements sur les espaces de nommage dans le Chapitre 10.

La classe exception dont bad_alloc hérite est déclarée comme suit dans le fichier d’en-tête exception:

class exception {

public:

exception (void) throw();

exception(const exception &) throw();

exception &operator=(const exception &) throw();

virtual ~exception(void) throw();

virtual const char *what(void) const throw();

};

Note :Contrairement aux en-têtes de la bibliothèque C standard, les noms des en-têtes de la bibliothèque standard C++ n’ont pas d’extension. Cela permet notamment d’éviter des conflits de noms avec les en-têtes existants.

Vous trouverez plus d’informations sur les exceptions dans le Chapitre 8.

Si vous désirez remplacer le gestionnaire par défaut, vous pouvez utiliser la fonction std::set_new_handler. Cette fonction attend en paramètre le pointeur sur le gestionnaire d’erreur à installer et renvoie le pointeur sur le gestionnaire d’erreur précédemment installé.

Les gestionnaires d’erreurs ne prennent aucun paramètre et ne renvoient aucune valeur. Leur comportement doit être le suivant :

Chapitre 7. C++ : la couche objet

soit ils prennent les mesures nécessaires pour permettre l’allocation du bloc de mémoire demandé et rendent la main à l’opérateurnew. Ce dernier refait alors une tentative pour allouer le bloc de mémoire. Si cette tentative échoue à nouveau, le gestionnaire d’erreur est rappelé. Cette boucle se poursuit jusqu’à ce que l’opération se déroule correctement ou qu’une exception std::bad_alloc soit lancée ;

soit ils lancent une exception de classe std::bad_alloc ;

soit ils terminent l’exécution du programme en cours.

La bibliothèque standard définit une version avec placement des opérateursnewetnew[], qui ren-voient le pointeur nul au lieu de lancer une exception en cas de manque de mémoire. Ces opérateurs prennent un deuxième paramètre, de type std::nothrow_t, qui doit être spécifié lors de l’appel. La bibliothèque standard définit un objet constant de ce type afin que les programmes puissent l’utiliser sans avoir à le définir eux-même. Cet objet se nommestd::nothrow

Exemple 7-23. Utilisation de new sans exception

char *data = new(std::nothrow) char[25];

if (data == NULL) {

// Traitement de l’erreur...

.. . }

Note :La plupart des compilateurs ne respectent pas les règles dictées par la norme C++. En effet, ils préfèrent retourner la valeur nulle en cas de manque de mémoire au lieu de lancer une exception. On peut rendre ces implémentations compatibles avec la norme en installant un gestionnaire d’erreur qui lance lui-même l’exception std::bad_alloc.