13. Création d'Objets Dynamiques▲
Parfois vous connaissez l'exacte quantité, le type, et la durée de vie des objets dans votre programme. Mais pas toujours.
Combien d'avions un système de contrôle du traffic aérien aura-t-il à gérer? Combien de formes différentes utilisera un système de DAO? Combien y aura-t-il de noeuds dans un réseau?
Pour résoudre ce problème général de programmation, il est essentiel d'être capable de créer et de détruire les objets en temps réel (en cours d'exécution). Bien entendu, C a toujours proposé les fonctions d' allocation de mémoire dynamiquemalloc( )et free( )(ainsi que quelques variantes de malloc( )) qui allouent de la mémoire sur le tas(également appelé espace de stockage libre) au moment de l'éxecution.
Cependant, ceci ne marchera tout simplement pas en C++. Le constructeur ne vous permet pas de manipuler l'adresse de la mémoire à initialiser ,et pour une bonne raison. Si vous pouviez faire cela, vous pourriez:
- Oublier. Dans ce cas l'initialisation des objets en C++ ne serait pas garantie.
- Accidentellement faire quelque chose à l'objet avant de l'initialiser, espérant que la chose attendue se produira.
- Installer un objet d'une taille inadéquate.
Et bien sûr, même si vous avez tout fait correctement, toute personne qui modifie votre programme est susceptible de faire les mêmes erreurs. Une initialisation incorrecte est responsable d'une grande part des problèmes de programmation, de sorte qu'il est spécialement important de garantir des appels de constructeurs pour les objets créés sur le tas.
Aussi comment C++ réussit-il à garantir une initialisation correcte ainsi qu'un nettoyage, tout en vous permettant de créer des objets dynamiquement sur le tas?
La réponse est: en apportant la création d'objet dynamique au coeur du langage. malloc( )et free( )sont des fonctions de bibliothèque, elles se trouvent ainsi en dehors du contrôle direct du compilateur. Toutefois, si vous avez un opérateurpour réaliser l'action combinée d'allocation dynamique et et d'initialisation et un autre opérateur pour accomplir l'action combinée de nettoyage et de restitution de mémoire, le compilateur peut encore guarantir que les constructeurs et les destructeurs seront appelés pour tous les objets.
Dans ce chapitre, vous apprendrez comment les newet deletede C++ résolvent élégamment ce problème en créant des objets sur le tas en toute sécurité.
13-1. Création d'objets▲
Lorsqu'un objet C++ est créé, deux événements ont lieu:
- Un espace mémoire est alloué pour l'objet.
- Le constructeur est appelé pour initialiser cette zone.
Maintenant vous devriez croire que la seconde étape a toujourslieu. C++ l'impose parce que les objets non initialisés sont une source majeure de bogues de programmes. Où et comment l'objet est créé n'a aucune importance – le constructeur est toujours appelé.
La première étape, cependant, peut se passer de plusieurs manières, ou à différents moments :
- L'espace peut être alloué avant que le programme commence, dans la zone de stockage statique. Ce stockage existe pour toute la durée du programme.
- L'allocation peut être créée sur la pile à chaque fois qu'un point d'exécution particulier est atteint (une accolade ouvrante). Ce stockage est libéré automatiquement au point d'exécution complémentaire (l'accolade fermante). Ces opérations d'allocation sur la pile font partie de la logique câblée du jeu d'instructions du processeur et sont très efficaces. Toutefois, il vous faut connaître exactement de combien de variables vous aurez besoin quand vous êtes en train d'écrire le programme de façon que le compilateur puisse générer le code adéquat.
- L'espace peut être alloué depuis un segment de mémoire appelé le 'tas' (aussi connu comme " free store"). Ceci est appelé l'allocation dynamique de la mémoire. Pour allouer cette mémoire, une fonction est appelée au moment de l'exécution ; cela signifie que vous pouvez décider à n'importe quel moment que vous voulez de la mémoire et combien vous en voulez. Vous êtes également responsable de déterminer quand libérer la mémoire, ce qui signifie que la durée de vie de cette mémoire peut être aussi longue que vous le désirez – ce n'est pas déterminé par la portée.
Souvent ces trois régions sont placées dans un seul segment contigu de mémoire physique : la zone statique, la pile, et le tas (dans un ordre déterminé par l'auteur du compilateur). Néammoins, il n'y a pas de règles. La pile peut être dans un endroit particulier, et le tas peut être implémenté en faisant des appels à des tronçons de mémoire depuis le système d'exploitation. En tant que programmeur, ces choses sont normallement protégées de vous, aussi la seule chose à laquelle vous avez besoin de penser est que la mémoire est là quand vous la demandez.
13-1-1. L'approche du C au tas▲
Pour allouer de la mémoire dynamiquement au moment de l'exécution, C fournit des fonctions dans sa bibliothèque standard : malloc( )et ses variantes calloc( )et realloc( )pour produire de la mémoire du tas, et free( )pour rendre la mémoire au tas. Ces fonctions sont pragmatiques mais primitives et nécessitent de la compréhension et du soin de la part du programmeur. Pour créer une instance d'une classe sur le tas en utilisant les fonctions de mémoire dynamique du C, il vous faudrait faire quelque chose comme ceci :
//: C13:MallocClass.cpp
// Malloc avec des classes
// Ce qu'il vous faudrait faire en l'absence de "new"
#include
"../require.h"
#include
<cstdlib>
// malloc() & free()
#include
<cstring>
// memset()
#include
<iostream>
using
namespace
std;
class
Obj {
int
i, j, k;
enum
{
sz =
100
}
;
char
buf[sz];
public
:
void
initialize() {
// impossible d'utiliser un constructeur
cout <<
"initialisation de Obj"
<<
endl;
i =
j =
k =
0
;
memset(buf, 0
, sz);
}
void
destroy() const
{
// impossible d'utiliser un destructeur
cout <<
"destruction de Obj"
<<
endl;
}
}
;
int
main() {
Obj*
obj =
(Obj*
)malloc(sizeof
(Obj));
require(obj !=
0
);
obj->
initialize();
// ... un peu plus tard :
obj->
destroy();
free(obj);
}
///
:~
Vous pouvez voir l'utilisation de malloc( )pour créer un stockage pour l'objet à la ligne :
Obj*
obj =
(Obj*
)malloc(sizeof
(Obj));
Ici, l'utilisateur doit déterminer la taille de l'objet (une occasion de se tromper). malloc( )retourne un void*parce qu'il produit un fragment de mémoire, pas un objet. C++ ne permet pas à un void*d'être affecté à tout autre pointeur, il doit donc être transtypé.
Parce que malloc( )peut échouer à trouver de la mémoire (auquel cas elle retourne zéro), vous devez vérifier le pointeur retourné pour vous assurer que l'opération a été un succès.
Mais le pire problème est cette ligne :
Obj->
initialize();
A supposer que les utilisateurs aient tout fait correctement jusque là, ils doivent se souvenir d'initialiser l'objet avant qu'il soit utilisé. Notez qu'un constructeur n'a pas été utilisé parce qu'un constructeur ne peut pas être appelé explicitement (50)– il est appelé pour vous par le compilateur quand un objet est créé. Le problème ici est que l'utilisateur a maintenant la possibilité d'oublier d'accomplir l'initialisation avant que l'objet soit utilisé, réintroduisant ainsi une source majeure d'erreurs.
Il s'avère également que de nombreux programmeurs semblent trouver les fonctions de mémoire dynamique du C trop peu claires et compliquées ; il n'est pas rare de trouver des programmeurs C qui utilisent des machines à mémoire virtuelle allouant de très grands tableaux de variables dans la zone statique pour éviter de réfléchir à l'allocation dynamique. Parce que C++ essaie de rendre l'utilisation de bibliothèques sûre et sans effort pour le programmeur occasionnel, l'approche de C à la mémoire dynamique n'est pas acceptable.
13-1-2. l'operateur new▲
La solution en C++ est de combiner toutes les actions nécessaires pour créer un objet en un unique opérateur appelé new. Quand vous créez un objet avec new(en utilisant une expression new), il alloue suffisamment d'espace sur le tas pour contenir l'objet et appelle le constructeur pour ce stockage. Ainsi, si vous dites
MyType *
fp =
new
MyType(1
,2
);
à l'exécution, l'équivalent de malloc(sizeof(MyType))est appelé (souvent, c'est littéralement un appel à malloc( )), et le constructeur pour MyTypeest appelé avec l'adresse résultante comme pointeur this, utilisant (1,2)comme liste d'arguments. A ce moment le pointeur est affecté à fp, c'est un objet vivant, initialisé ; vous ne pouvez même pas mettre vos mains dessus avant cela. C'est aussi automatiquement le type MyTypeadéquat de sorte qu'aucun transtypage n'est nécessaire.
l'opérateur newpar défaut vérifie pour s'assurer que l'allocation de mémoire est réussie avant de passer l'adresse au constructeur, de sorte que vous n'avez pas à déterminer explicitement si l'appel a réussi. Plus tard dans ce chapitre, vous découvrirez ce qui se passe s'il n'y a plus de mémoire disponible.
Vous pouvez créer une expression-new en utilisant n'importe quel constructeur disponible pour la classe. Si le constructeur n'a pas d'arguments, vous écrivez l'expression-new sans liste d'argument du constructeur:
MyType *
fp =
new
MyType;
Remarquez comment le processus de création d'objets sur le tas devient simple ; une simple expression, avec tout les dimensionnements, les conversions et les contrôles de sécurité integrés. Il est aussi facile de créer un objet sur le tas que sur la pile.
13-1-3. l'opérateur delete▲
Le complément de l'expression-new est l' expression-delete, qui appelle d'abord le destructeur et ensuite libère la mémoire (souvent avec un appel à free( )). Exactement comme une expression-new retourne un pointeur sur l'objet, une expression-delete nécessite l'adresse d'un objet.
delete
fp;
Ceci détruit et ensuite libère l'espace pour l'objet MyTypealloué dynamiquement qui a été créé plus tôt.
deletepeut être appelé seulement pour un objet créé par new. Si vous malloc(ez)( )(ou calloc(ez)( )ou realloc(ez)( )) un objet et ensuite vous le delete(z), le comportement est indéfini. Parce que la plupart des implémentations par défaut de newet deleteutilisent malloc( )et free( ), vous finirez probablement par libérer la mémoire sans appeler le destructeur.
Si le pointeur que vous détruisez vaut zéro, rien ne se passera. Pour cette raison, les gens recommandent souvent de mettre un pointeur à zero immédiatemment après un 'delete', pour empêcher de le détruire deux fois. Détruire un objet plus d'une fois est assurement une mauvaise chose à faire, et causera des problèmes.
13-1-4. Un exemple simple▲
Cet exemple montre que l'initialisation a lieu :
//: C13:Tree.h
#ifndef TREE_H
#define TREE_H
#include
<iostream>
class
Tree {
int
height;
public
:
Tree(int
treeHeight) : height(treeHeight) {}
~
Tree() {
std::
cout <<
"*"
; }
friend
std::
ostream&
operator
<<
(std::
ostream&
os, const
Tree*
t) {
return
os <<
"La hauteur de l'arbre est : "
<<
t->
height <<
std::
endl;
}
}
;
#endif
// TREE_H ///:~
//: C13:NewAndDelete.cpp
// Démo simple de new & delete
#include
"Tree.h"
using
namespace
std;
int
main() {
Tree*
t =
new
Tree(40
);
cout <<
t;
delete
t;
}
///
:~
Vous pouvez prouver que le consructeur est appelé en affichant la valeur de Tree. Ici, c'est fait en surchargeant l'opérateur operator<<pour l'utiliser avec un ostreamet un Tree*. Notez, toutefois, que même si la fonction est déclarée comme friend, elle est définie 'inline' ! C'est pour des raisons d'ordre purement pratique– la définition d'une fonction amied'une classe comme une 'inline' ne change pas le statut d' amieou le fait que c'est une fonction globale et non une fonction membre de classe. Notez également que la valeur de retour est le résultat de toute l'expression de sortie, qui est un ostream&(ce qu'il doit être, pour satisfaire le type de valeur de retour de la fonction).
13-1-5. Le surcoût du gestionnaire de mémoire▲
Lorsque vous créez des objets automatiques sur la pile, la taille des objets et leur durée de vie sont intégrées immédiatement dans le code généré, parce que le compilateur connaît le type exact, la quantité, et la portée. La création d'objets sur le tas implique un surcoût à la fois dans le temps et dans l'espace. Voici un scenario typique. (Vous pouvez remplacer malloc( )par calloc( )ou realloc( ).)
Vous appelez malloc( ), qui réclame un bloc de mémoire dans le tas. (Ce code peut, en réalité, faire partie de malloc( ).)
Une recherche est faite dans le tas pour trouver un bloc de mémoire suffisamment grand pour satisfaire la requête. Cela est fait en consultant une carte ou un répertoire de quelque sorte montrant quels blocs sont actuellement utilisés et quels blocs sont disponibles. C'est un procédé rapide, mais qui peut nécessiter plusieurs essais, aussi il peut ne pas être déterministe – c'est à dire que vous ne pouvez pas compter sur le fait que malloc( )prenne toujours le même temps pour accomplir son travail.
Avant qu'un pointeur sur ce bloc soit retourné, la taille et l'emplacement du bloc doivent être enregistrés de sorte que des appels ultérieurs à malloc( )ne l'utiliseront pas, et de sorte que lorsque vous appelez free( ), le système sache combien de mémoire libérer.
La façon dont tout cela est implémenté peut varier dans de grandes proportions. Par exemple, rien n'empêche que des primitives d'allocations de mémoire soit implémentées dans le processeur. Si vous êtes curieux, vous pouvez écrire des programmes de test pour essayer de deviner la façon dont votre malloc( )est implémenté. Vous pouvez aussi lire le code source de la bibliothèque, si vous l'avez (les sources GNU sont toujours disponibles).
13-2. Exemples précédents revus▲
En utilisant newet delete, l'exemple Stashintroduit précédemment dans ce livre peut être réécrit en utilisant toutes les fonctionalités présentées dans ce livre jusqu'ici. Le fait d'examiner le nouveau code vous donnera également une revue utile de ces sujets .
A ce point du livre, ni la classe Stashni la classe Stackne “posséderont” les objets qu'elles pointent ; c'est à dire que lorsque l'objet Stashou Stacksort de la portée, il n'appellera pas deletepour tous les objets qu'il pointe. La raison pour laquelle cela n'est pas possible est que, en tentant d'être génériques, ils conservent des pointeurs void. Si vous detruisez un pointeur void, la seule chose qui se produit est la libération de la mémoire, parce qu'il n'y a pas d'informaton de type et aucun moyen pour le compilateur de savoir quel destructeur appeler.
13-2-1. detruire un void* est probablement une erreur▲
Cela vaut la peine de relever que si vous appelez deletesur un void*, cela causera presque certainement une erreur dans votre programme à moins que la destination de ce pointeur ne soit très simple ; en particulier, il ne doit pas avoir de destructeur. Voici un exemple pour vous montrer ce qui se passe :
//: C13:BadVoidPointerDeletion.cpp
// detruire des pointeurs void peut provoquer des fuites de mémoire
#include
<iostream>
using
namespace
std;
class
Object {
void
*
data; // un certain stockage
const
int
size;
const
char
id;
public
:
Object(int
sz, char
c) : size(sz), id(c) {
data =
new
char
[size];
cout <<
"Construction de l'objet "
<<
id
<<
", taille = "
<<
size <<
endl;
}
~
Object() {
cout <<
"Destruction de l'objet "
<<
id <<
endl;
delete
[]data; // OK libérer seulement la donnée,
// aucun appel de destructeur nécessaire
}
}
;
int
main() {
Object*
a =
new
Object(40
, 'a'
);
delete
a;
void
*
b =
new
Object(40
, 'b'
);
delete
b;
}
///
:~
La classe Objectcontient un void*qui est initialisé pour une donnée “brute” (il ne pointe pas sur des objets ayant un destructeur). Dans le destructeur de Object, deleteest appelé pour ce void*avec aucun effet néfaste, parce que la seule chose que nous ayons besoin qu'il se produise est que cette mémoire soit libérée.
Cependant, dans main( )vous pouvez voir qu'il est tout à fait nécessaire que deletesache avec quel type d'objet il travaille. Voici la sortie :
Construction de l'objet a, taille =
40
Destruction de l'objet a
Construction de l'objet b, taille =
40
Parce que delete asait que apointe sur un Object, le destructeur est appelé et le stockage alloué pour dataest libérée. Toutefois, si vous manipulez un objet à travers un void*comme dans le cas de delete b, la seule chose qui se produit est que la mémoire pour l' Objectest libérée ; mais le destructeur n'est pas appelé de sorte qu'il n'y a pas libération de la mémoire que datapointe. A la compilation de ce programme, vous ne verrez probablement aucun message d'avertissement ; le compilateur suppose que vous savez ce que vous faites. Aussi vous obtenez une fuite de mémoire très silencieuse.
Si vous avez une fuite de mémoire dans votre programme, recherchez tous les appels à deleteet vérifiez le type de pointeur qui est détruit. Si c'est un void*alors vous avez probablement trouvé une source de votre fuite de mémoire (C++ offre, cependant, d'autres possibilités conséquentes de fuites de mémoire).
13-2-2. La responsabilité du nettoyage avec les pointeurs▲
Pour rendre les conteneurs Stashet Stackflexibles (capables de contenir n'importe quel type d'objet), ils conserveront des pointeurs void. Ceci signifie que lorsqu'un pointeur est retourné par l'objet Stashou Stack, vous devez le transtyper dans le type convenablebavant de pouvoir l'utiliser ; comme vu auparavant, vous devez également le transtyper dans le type convenable avant de le detruire ou sinon vous aurez une fuite de mémoire.
L'autre cas de fuite de mémoire concerne l'assurance que deleteest effectivement appelé pour chaque pointeur conservé dans le conteneur. Le conteneur ne peut pas “posséder” le pointeur parce qu'il le détient comme un void*et ne peut donc pas faire le nettoyage correct. L'utilisateur doit être responsable du nettoyage des objets. Ceci produit un problème sérieux si vous ajoutez des pointeurs sur des objets créés dans la pile etdes objets créés sur le tas dans le même conteneur parce qu'une expression-delete est dangereuse pour un pointeur qui n'a pas été créé sur le tas. (Et quand vous récupérez un pointeur du conteneur, comment saurez vous où son objet a été alloué ?) Ainsi, vous devez vous assurer que les objets conservés dans les versions suivantes de Stashet Stackont été créés seulement sur le tas, soit par une programmation méticuleuse ou en créant des classes ne pouvant être construites que sur le tas.
Il est également important de s'assurer que le programmeur client prend la responsabilité du nettoyage de tous les pointeurs du conteneur. Vous avez vu dans les exemples précédents comment la classe Stackvérifie dans son destructeur que tous les objets Linkont été dépilés. Pour un Stashde pointeurs, toutefois, une autre approche est nécessaire.
13-2-3. Stash pour des pointeurs▲
Cette nouvelle version de la classe Stash, nommée PStash, détient des pointerssur des objets qui existent eux-mêmes sur le tas, tandis que l'ancienne Stashdans les chapitres précédents copiait les objets par valeur dans le conteneur Stash. En utilisant newet delete, il est facile et sûr de conserver des pointeurs vers des objets qui ont été créés sur le tas.
Voici le fichier d'en-tête pour le “ Stashde pointeurs”:
//: C13:PStash.h
// Conserve des pointeurs au lieu d'objets
#ifndef PSTASH_H
#define PSTASH_H
class
PStash {
int
quantity; //Nombre d'espaces mémoire
int
next; // Espace vide suivant
// Stockage des pointeurs:
void
**
storage;
void
inflate(int
increase);
public
:
PStash() : quantity(0
), storage(0
), next(0
) {}
~
PStash();
int
add(void
*
element);
void
*
operator
[](int
index) const
; // Récupération
// Enlever la référence de ce PStash:
void
*
remove(int
index);
// Nombre d'éléments dans le Stash:
int
count() const
{
return
next; }
}
;
#endif
// PSTASH_H ///:~
Les éléments de données sous-jacents sont assez similaires, mais maintenant le stockageest un tableau de pointeurs void, et l'allocation de mémoire pour ce tableau est réalisé par newau lieu de malloc( ). Dans l'expression
void
**
st =
new
void
*
[quantity +
increase];
le type d'objet alloué est un void*, ainsi l'expression alloue un tableau de pointeurs void.
Le destructeur libère la mémoire où les pointeurs voidsont stockés plutôt que d'essayer de libérer ce qu'ils pointent (ce qui, comme on l'a remarqué auparavant, libérera leur stockage et n'appellera pas les destructeurs parce qu'un pointeur voidne comporte aucune information de type).
L'autre changement est le remplacement de la fonction fetch( )par operator[ ], qui est syntaxiquement plus expressif. De nouveau, toutefois, un void*est retourné, de sorte que l'utilisateur doit se souvenir des types rassemblés dans le conteneur et transtyper les pointeurs au fur et à mesure de leur récupération (un problème auquel nous apporterons une solution dans les chapitres futurs).
Voici les définitions des fonctions membres :
//: C13:PStash.cpp {O}
// définitions du Stash de Pointeurs
#include
"PStash.h"
#include
"../require.h"
#include
<iostream>
#include
<cstring>
// fonctions 'mem'
using
namespace
std;
int
PStash::
add(void
*
element) {
const
int
inflateSize =
10
;
if
(next >=
quantity)
inflate(inflateSize);
storage[next++
] =
element;
return
(next -
1
); // Indice
}
// Pas de propriété:
PStash::
~
PStash() {
for
(int
i =
0
; i <
next; i++
)
require(storage[i] ==
0
,
"PStash n'a pas été nettoyé"
);
delete
[]storage;
}
// Surcharge d'opérateur pour accès
void
*
PStash::
operator
[](int
index) const
{
require(index >=
0
,
"PStash::operator[] indice négatif"
);
if
(index >=
next)
return
0
; // Pour signaler la fin
// Produit un pointeur vers l'élément désiré:
return
storage[index];
}
void
*
PStash::
remove(int
index) {
void
*
v =
operator
[](index);
// "Enlève" le pointeur:
if
(v !=
0
) storage[index] =
0
;
return
v;
}
void
PStash::
inflate(int
increase) {
const
int
psz =
sizeof
(void
*
);
void
**
st =
new
void
*
[quantity +
increase];
memset(st, 0
, (quantity +
increase) *
psz);
memcpy(st, storage, quantity *
psz);
quantity +=
increase;
delete
[]storage; // Ancien emplacement
storage =
st; // Pointe sur un nouvel emplacement
}
///
:~
La fonction add( )est effectivement la même qu'avant, sauf qu'un pointeur est stocké plutôt qu'une copie de l'objet entier.
Le code de inflate( )est modifié pour gérer l'allocation d'un tableau de void*à la différence de la conception précédente, qui ne fonctionnait qu'avec des octets 'bruts'. Ici, au lieu d'utiliser l'approche précédente de copier par indexation de tableau, la fonction de la librairie standard C memset( )est d'abord utilisée pour mettre toute la nouvelle mémoire à zéro (ceci n'est pas strictement nécessaire, puisqu'on peut supposer que le PStashgère toute la mémoire correctement– mais en général cela ne fait pas de mal de prendre quelques précautions supplémentaires). Ensuite memcpy( )déplace les données existantes de l'ancien emplacement vers le nouveau. Souvent, des fonctions comme memset( )et memcpy( )ont été optimisées avec le temps, de sorte qu'elles peuvent être plus rapides que les boucles montrées précédemment. Mais avec une fonction comme inflate( )qui ne sera probablement pas utilisée très souvent il se peut que vous ne perceviez aucune différence de performance. Toutefois, le fait que les appels de fonction soient plus concis que les boucles peut aider à empêcher des erreurs de codage.
Pour placer la responsabilité du nettoyage d'objets carrément sur les épaules du programmeur client, il y a deux façons d'accéder aux pointeurs dans le PStash: l' operator[], qui retourne simplement le pointeur mais le conserve comme membre du conteneur, et une seconde fonction membre remove( ), qui retourne également le pointeur, mais qui l'enlève également du conteneur en affectant zéro à cette position. Lorsque le destructeur de PStashest appelé, il vérifie pour s'assurer que tous les pointeurs sur objets ont été enlevés ; dans la négative, vous êtes prévenu pour pouvoir empêcher une fuite de mémoire (des solutions plus élégantes viendront à leur tour dans les chapitres suivants).
Un test
Voici le vieux programme de test pour Stashrécrit pour PStash:
//: C13:PStashTest.cpp
//{L} PStash
// Test du Stash de pointeurs
#include
"PStash.h"
#include
"../require.h"
#include
<iostream>
#include
<fstream>
#include
<string>
using
namespace
std;
int
main() {
PStash intStash;
// 'new' fonctionnne avec des types prédéfinis, également. Notez
// la syntaxe "pseudo-constructor":
for
(int
i =
0
; i <
25
; i++
)
intStash.add(new
int
(i));
for
(int
j =
0
; j <
intStash.count(); j++
)
cout <<
"intStash["
<<
j <<
"] = "
<<
*
(int
*
)intStash[j] <<
endl;
// Nettoyage :
for
(int
k =
0
; k <
intStash.count(); k++
)
delete
intStash.remove(k);
ifstream in ("PStashTest.cpp"
);
assure(in, "PStashTest.cpp"
);
PStash stringStash;
string line;
while
(getline(in, line))
stringStash.add(new
string(line));
// Affiche les chaînes :
for
(int
u =
0
; stringStash[u]; u++
)
cout <<
"stringStash["
<<
u <<
"] = "
<<
*
(string*
)stringStash[u] <<
endl;
// Nettoyage :
for
(int
v =
0
; v <
stringStash.count(); v++
)
delete
(string*
)stringStash.remove(v);
}
///
:~
Commes auparavant, les Stashs sont créés et remplis d'information, mais cette fois l'information est constituée des pointeurs résultant d'expressions- new. Dans le premier cas, remarquez la ligne:
intStash.add(new
int
(i));
L'expression new int(i)utilise la forme pseudo-constructeur, ainsi le stockage pour un nouvel objet intest créé sur le tas, et le intest initialisé avec la valeur i.
Pendant l'affichage, la valeur retournée par PStash::operator[]doit être transtypée dans le type adéquat; ceci est répété pour le reste des autres objets PStashdans le programme. C'est un effet indésirable d'utiliser des pointeurs voidcomme représentation sous-jacente et ce point sera résolu dans les chapitres suivants.
Le second test ouvre le fichier du code source et le lit ligne par ligne dans un autre PStash. Chaque ligne est lue dans un objet stringen utilisant getline( ), ensuite un nouveaustringest créé depuis linepour faire une copie indépendante de cette ligne. Si nous nous étions contentés de passer l'adresse de lineà chaque fois, nous nous serions retrouvés avec un faisceau de pointeurs pointant tous sur line, laquelle contiendrait la dernière ligne lue du fichier.
Lorsque vous récupérez les pointeurs, vous voyez l'expression :
*
(string*
)stringStash[v]
Le pointeur retourné par operator[]doit être transtypé en un string*pour lui donner le type adéquat. Ensuite, le string*est déréférencé de sorte que l'expression est évaluée comme un objet, à ce moment le compilateur voit un objet stringà envoyer à cout.
Les objets créés sur le tas doivent être détruits par l'utilisation de l'instruction remove( )ou autrement vous aurez un message au moment de l'exécution vous disant que vous n'avez pas complètement nettoyé les objets dans le PStash. Notez que dans le cas des pointeurs sur int, aucun transtypage n'est nécessaire parce qu'il n'y a pas de destructeur pour un intet tout ce dont nous avons besoin est une libération de la mémoire :
delete
intStash.remove(k);
Néammoins, pour les pointeurs sur string, si vous oubliez de transtyper vous aurez une autre fuite de mémoire (silencieuse), de sorte que le transtypage est essentiel :
delete
(string*
)stringStash.remove(k);
Certains de ces problèmes (mais pas tous) peuvent être résolus par l'utilisation des 'templates' (que vous étudierez dans le chapitre 16).
13-3. new & delete pour les tableaux▲
En C++, vous pouvez créer des tableaux d'objets sur la pile ou sur le tas avec la même facilité, et (bien sûr) le constructeur est appelé pour chaque objet dans le tableau. Il y a une contrainte, toutefois : il doit y avoir un constructeur par défaut, sauf pour l'initialisation d'agrégat sur la pile (cf. Chapitre 6), parce qu'un constructeur sans argument doit être appelé pour chaque objet.
Quand vous créez des tableaux d'objets sur le tas en utilisant new, vous devez faire autre chose. Voici un exemple d'un tel tableau :
MyType*
fp =
new
MyType[100
];
Ceci alloue suffisamment d'espace de stockage sur le tas pour 100 objets MyTypeet appelle le constructeur pour chacun d'eux. Cependant, à présent vous ne disposez que d'un MyType*, ce qui est exactement la même chose que vous auriez eu si vous aviez dit :
MyType*
fp2 =
new
MyType;
pour créer un seul objet. Parce que vous avez écrit le code, vous savez que fpest en fait l'adresse du début d'un tableau, et il paraît logique de sélectionner les éléments d'un tableau en utilisant une expression comme fp[3]. Mais que se passe-t-il quand vous détruisez le tableau ? Les instructions
delete
fp2; // OK
delete
fp; // N'a pas l'effet désiré
paraissent identiques, et leur effet sera le même. Le destructeur sera appelé pour l'objet MyTypepointé par l'adresse donnée, et le stockage sera libéré. Pour fp2cela fonctionne, mais pour fpcela signifie que 99 appels au destructeur ne seront pas effectués. La bonne quantité d'espace de stockage sera toujours libérée, toutefois, parce qu'elle est allouée en un seul gros morceau, et la taille de ce morceau est cachée quelque part par la routine d'allocation.
La solution vous impose de donner au compilateur l'information qu'il s'agit en fait de l'adresse du début d'un tableau. Ce qui est fait avec la syntaxe suivante :
delete
[]fp;
Les crochets vides disent au compilateur de générer le code qui cherche le nombre d'objets dans le tableau, stocké quelque part au moment de la création de ce dernier, et appelle le destructeur pour ce nombre d'objets. C'est en fait une version améliorée de la syntaxe ancienne, que vous pouvez toujours voir dans du vieux code :
delete
[100
]fp;
qui forçait le programmeur à inclure le nombre d'objets dans le tableau et introduisait le risque que le programmeur se trompe. Le fait de laisser le compilateur gérer cette étape consommait un temps système supplémentaire très bas, et il a été décidé qu'il valait mieux ne préciser qu'en un seul endroit plutôt qu'en deux le nombre d'objets.
13-3-1. Rendre un pointeur plus semblable à un tableau▲
A côté de cela, le fpdéfini ci-dessus peut être modifié pour pointer sur n'importe quoi, ce qui n'a pas de sens pour l'adresse de départ d'un tableau. Il est plus logique de le définir comme une constante, si bien que toute tentative de modifier le pointeur sera signalée comme une erreur. Pour obtenir cet effet, vous pouvez essayer
int
const
*
q =
new
int
[10
];
ou bien
const
int
*
q =
new
int
[10
];
mais dans les deux cas, le constsera lié au int, c'est-à-dire ce qui estpointé, plutôt qu'à la qualité du pointeur lui-même. Au lieu de cela, vous devez dire :
int
*
const
q =
new
int
[10
];
A présent, les éléments du tableau dans qpeuvent être modifiés, mais tout changement de q(comme q++) est illégal, comme c'est le cas avec un identifiant de tableau ordinaire.
13-4. Manquer d'espace de stockage▲
Que se passe-t-il quand l' operator new ()ne peut trouver de block de mémoire contigü suffisamment grand pour contenir l'objet désiré ? Une fonction spéciale appelée le gestionnaire de l'opérateur newest appelée. Ou plutôt, un pointeur vers une fonction est vérifié, et si ce pointeur ne vaut pas zéro la fonction vers laquelle il pointe est appelée.
Le comportement par défaut pour ce gestionnaire est de lancer une exception, sujet couvert dans le deuxième Volume. Toutefois, si vous utilisez l'allocation sur le tas dans votre programme, il est prudent de remplacer au moins le gestionnaire de 'new' avec un message qui dit que vous manquez de mémoire et ensuite termine le programme. Ainsi, pendant le débugage, vous aurez une idée de ce qui s'est produit. Pour le programme final vous aurez intérêt à utiliser une récupération plus robuste.
Vous remplacez le gestionnaire de 'new' en incluant new.het en appelant ensuite set_new_handler( )avec l'adresse de la fonction que vous voulez installer :
//: C13:NewHandler.cpp
// Changer le gestionnaire de new
#include
<iostream>
#include
<cstdlib>
#include
<new>
using
namespace
std;
int
count =
0
;
void
out_of_memory() {
cerr <<
"memory exhausted after "
<<
count
<<
" allocations!"
<<
endl;
exit(1
);
}
int
main() {
set_new_handler(out_of_memory);
while
(1
) {
count++
;
new
int
[1000
]; // Epuise la mémoire
}
}
///
:~
La fonction gestionnaire de 'new' ne doit prendre aucun argument et avoir une valeur de retour void. La boucle whilecontinuera d'allouer des objets int(et de jeter leurs adresses de retour) jusqu'à ce que le stockage libre soit épuisé. A l'appel à newqui suit immédiatement, aucun espace de stockage ne peut être alloué, et le gestionnaire de 'new' sera appelé.
Le comportement du gestionnaire de 'new' est lié à operator new ( ), donc si vous surchargez operator new ( )(couvert dans la section suivante) le gestionnaire de 'new' ne sera pas appelé par défaut. Si vous voulez toujours que le gestionnaire de 'new' soit appelé vous devrez écrire le code pour ce faire dans votre operator new ( )surchargé.
Bien sûr, vous pouvez écrire des gestionnaires de 'new' plus sophistiqués, voire même un qui essaye de réclamer de la mémoire (généralement connu sous le nom de ramasse-miettes( garbage collectoren anglais, ndt)). Ce n'est pas un travail pour un programmeur novice.
13-5. Surcharger new & delete▲
Quand vous créez une expression new, deux choses se produisent. D'abord, l'espace de stockage est alloué en utilisant operator new( ), puis le constructeur est appelé. Dans une expression delete, le destructeur est appelé, puis le stockage est libéré en utilisant operator delete( ). Les appels au constructeur et au destructeur ne sont jamais sous votre contrôle (autrement vous pourriez les corrompre accidentellement), mais vous pouvezmodifier les fonctions d'allocation de stockage operator new( )et operator delete( ).
Le système d'allocation de mémoire utilisé par newet deleteest conçu pour un usage général. Dans des situations spéciales, toutefois, il ne convient pas à vos besoin. La raison la plus commune pour modifier l'allocateur est l'efficacité : vous pouvez créer et détruire tellement d'objets d'une classe donnée que cela devient goulet d'étranglement en terme de vitesse. Le C++ vous permet de surcharger newet deletepour implémenter votre propre schéma d'allocation de stockage, afin que vous puissiez gérer ce genre de problèmes.
Un autre problème est la fragmentation du tas. En allouant des objets de taille différente, il est possible de fragmenter le tas si bien que vous vous retrouvez à cours de stockage. En fait, le stockage peut être disponible, mais à cause de la fragmentation aucun morceau n'est suffisamment grand pour satisfaire vos besoins. En créant votre propre allocateur pour une classe donnée, vous pouvez garantir que cela ne se produira jamais.
Dans les systèmes embarqués et temps-réel, un programme peut devoir tourner pendant longtemps avec des ressources limitées. Un tel système peut également nécessiter que l'allocation de mémoire prennent toujours le même temps, et il n'y a aucune tolérance à l'épuisement du tas ou à sa fragmentation. Un allocateur de mémoire sur mesure est la solution ; autrement, les programmeurs éviteront carrémnt d'utiliser newet deletedans ce genre de situations et manqueront un des atouts précieux du C++.
Quand vous surchargez operator new( )et operator delete( ), il est important de se souvenir que vous modifiez uniquement la façon dont l'espace de stockage brut est alloué. Le compilateur appellera simplement votre newau lieu de la version par défaut pour allouer le stockage, puis appellera le constructeur pour ce stockage. Donc, bien que le compilateur alloue le stockage etappelle le constructeur quand il voit new, tout ce que vous pouvez changer quand vous surchargez newest le volet allocation de la mémoire. ( deletea la même limitation.)
Quand vous surchargez operator new( ), vous remplacez également le comportement quand il en vient à manquer de mémoire, vous devez donc décider que faire dans votre operator new( ): renvoyer zéro, écrire une boucle pour appeler le gestionnaire de new et réessayer l'allocation, ou (typiquement) de lancer une exception bad_alloc(traitée dans le Volume 2, disponible sur www.BruceEckel.comen anglais et bientôt sur www.developpez.comen français).
Surcharger newet deletec'est comme surcharger n'importe quel autre opérateur. Toutefois, vous avez le choix de surcharger l'allocateur global ou d'utiliser un allocateur différent pour une classe donnée.
13-5-1. La surcharge globale de new & delete▲
C'est l'approche extrême, quand les versions globales de newet deletene sont pas satisfaisantes pour l'ensemble du système. Si vous surchargez les versions globales, vous rendez les versions par défaut complètement innaccessibles – vous ne pouvez même pas les appeler depuis vos redéfinitions.
Le newsurchargé doit prendre un argument de type size_t(le type standard pour les tailles en C standard). Cet argument est généré et vous est passé par le compilateur et représente la taille de l'objet que vous avez la charge d'allouer. Vous devez renvoyer un pointeur soit vers un objet de cette taille (ou plus gros, si vous avez des raisons pour ce faire), ou zéro si vous ne pouvez pas trouver la mémoire (auquel cas le constructeur n'est pasappelé !). Toutefois, si vous ne pouvez pas trouver la mémoire, vous devriez probablement faire quelque chose de plus informatif que simplement renvoyer zéro, comme d'appeler new-handler ou de lancer une exception, pour signaler qu'il y a un problème.
La valeur de retour de operator new( )est un void*, pasun pointeur vers un quelconque type particulier. Tout ce que vous avez fait est allouer de la mémoire, pas un objet fini – cela ne se produit pas tant que le constructeur n'est pas appelé, une action que le compilateur garantit et qui échappe à votre contrôle.
operator delete( )prend un void*vers la mémoire qui a été allouée par operator new. C'est un void*parce que operator deletene reçoit le pointeur qu' aprèsque le destructeur ait été appelé, ce qui efface le caractère objet du fragment de stockage. Le type de retour est void.
Voici un exemple simple montrant comment surcharger le newet le deleteglobaux :
//: C13:GlobalOperatorNew.cpp
// Surcharge new/delete globaux
#include
<cstdio>
#include
<cstdlib>
using
namespace
std;
void
*
operator
new
(size_t sz) {
printf("operateur new: %d octets
\n
"
, sz);
void
*
m =
malloc(sz);
if
(!
m) puts("plus de memoire"
);
return
m;
}
void
operator
delete
(void
*
m) {
puts("operateur delete"
);
free(m);
}
class
S {
int
i[100
];
public
:
S() {
puts("S::S()"
); }
~
S() {
puts("S::~S()"
); }
}
;
int
main() {
puts("creation & destruction d'un int"
);
int
*
p =
new
int
(47
);
delete
p;
puts("creation & destruction d'un s"
);
S*
s =
new
S;
delete
s;
puts("creation & destruction de S[3]"
);
S*
sa =
new
S[3
];
delete
[]sa;
}
///
:~
Ici vous pouvez voir la forme générale de la surcharge de newet delete. Ceux-ci utilisent les fonctions de librairie du C standard malloc( )et free( )pour les allocateurs (qui sont probablement ce qu'utilisent également les newet deletepar défaut !). Toutefois, ils affichent également des messages à propos de ce qu'ils font. Remarquez que printf( )et puts( )sont utilisés plutôt que iostreams. C'est parce que quand un objet iostreamest créé (comme les cin, coutet cerrglobaux), il appelle newpour allouer de la mémoire. Avec printf( )vous ne vous retrouvez pas dans un interblocage parce qu'il n'appelle pas newpour s'initialiser.
Dans main( ), des objets de type prédéfinis sont créés pour prouver que le newet le deletesurchargés sont également appelés dans ce cas. Puis un unique objet de type Sest créé, suivi par un tableau de S. Pour le tableau, vous verrez par le nombre d'octets réclamés que de la mémoire supplémentaire est allouée pour stocker de l'information (au sein du tableau) à propos du nombre d'objets qu'il contient. Dans tous les cas, les versions surchargées globales de newet deletesont utilisées.
13-5-2. Surcharger new & delete pour une classe▲
Bien que vous n'ayez pas à dire explicitement static, quand vous surchargez newet deletepour une classe, vous créez des fonctions membres static. Comme précédemment, la syntaxe est la même que pour la surcharge de n'importe quel opérateur. Quand le compilateur voit que vous utilisez newpour créer un objet de votre classe, il choisit le membre operator new( )de préférence à la définition globale. Toutefois, les versions globales de newet deletesont utilisées pour tous les autres types d'objets (à moins qu'ils n'aient leur propres newet delete).
Dans l'exemple suivant, un système d'allocation primitif est créé pour la classe Framis. Un tronçon de mémoire est réservé dans la zone de données statiques au démarrage du programme, et cette mémoire est utilisée pour allouer de l'espace pour les objets de type Framis. Pour déterminer quels blocs ont été alloués, un simple tableau d'octets est utilisé, un octet pour chaque bloc :
//: C13:Framis.cpp
// Surcharge locale de new & delete
#include
<cstddef>
// Size_t
#include
<fstream>
#include
<iostream>
#include
<new>
using
namespace
std;
ofstream out("Framis.out"
);
class
Framis {
enum
{
sz =
10
}
;
char
c[sz]; // Pour occuper de l'espace, pas utilisé
static
unsigned
char
pool[];
static
bool
alloc_map[];
public
:
enum
{
psize =
100
}
; // nombre de framis autorisés
Framis() {
out <<
"Framis()
\n
"
; }
~
Framis() {
out <<
"~Framis() ... "
; }
void
*
operator
new
(size_t) throw
(bad_alloc);
void
operator
delete
(void
*
);
}
;
unsigned
char
Framis::
pool[psize *
sizeof
(Framis)];
bool
Framis::
alloc_map[psize] =
{
false
}
;
// La taille est ignorée -- suppose un objet Framis
void
*
Framis::
operator
new
(size_t) throw
(bad_alloc) {
for
(int
i =
0
; i <
psize; i++
)
if
(!
alloc_map[i]) {
out <<
"utilise le bloc "
<<
i <<
" ... "
;
alloc_map[i] =
true
; // le marquer utilisé
return
pool +
(i *
sizeof
(Framis));
}
out <<
"plus de memoire"
<<
endl;
throw
bad_alloc();
}
void
Framis::
operator
delete
(void
*
m) {
if
(!
m) return
; // Vérifie si le pointeur est nul
// Suppose qu'il a été créé dans la réserve
// Calcule le numéro du bloc :
unsigned
long
block =
(unsigned
long
)m
-
(unsigned
long
)pool;
block /=
sizeof
(Framis);
out <<
"libère le bloc "
<<
block <<
endl;
// le marque libre :
alloc_map[block] =
false
;
}
int
main() {
Framis*
f[Framis::
psize];
try
{
for
(int
i =
0
; i <
Framis::
psize; i++
)
f[i] =
new
Framis;
new
Framis; // Plus de mémoire
}
catch
(bad_alloc) {
cerr <<
"Plus de mémoire !"
<<
endl;
}
delete
f[10
];
f[10
] =
0
;
// Utilise la mémoire libérée :
Framis*
x =
new
Framis;
delete
x;
for
(int
j =
0
; j <
Framis::
psize; j++
)
delete
f[j]; // Delete f[10] OK
}
///
:~
La réserve de mémoire pour le tas de Framisest créé en allouant un tableau d'octets suffisamment grand pour contenir psizeobjets Framis. La carte d'allocation fait psizeéléments de long, et il y a donc un boolpour chaque bloc. Toutes les valeurs dans la carte d'allocation sont initialisées à falseen utilisant l'astuce de l'initialisation d'agrégats qui consiste à affecter le premier élément afin que le compilateur initialise automatiquement tout le reste à leur valeur par défaut (qui est false, dans le cas des bool).
L' operator new( )local a la même syntaxe que le global. Tout ce qu'il fait est de chercher une valeur falsedans la carte d'allocation, puis met cette valeur à truepour indiquer qu'elle a été allouée et renvoie l'adresse du bloc de mémoire correspondant. S'il ne peut trouver aucune mémoire, il émet un message au fichier trace et lance une exception bad_alloc.
C'est le premier exemple d'exceptions que vous avez vu dans ce livre. Comme la discussion détaillée est repoussée au Volume 2, c'en est un usage très simple. Dans l' operator new( )il y a deux utilisations de la gestion d'exceptions. Premièrement, la liste d'arguments de la fonction est suivie par throw(bad_alloc), qui dit au compilateur et au lecteur que cette fonction peut lancer une exception de type bad_alloc. Deuxièmement, s'il n'y a plus de mémoire la fonction lance effectivement l'exception dans l'instruction throw bad_alloc. Quand une exception est lancée, la fonction cesse de s'exécuter et le contrôle est passé à un gestionnaire d'exécution, qui est exprimé par une clause catch.
Dans main( ), vous voyez l'autre partie de la figure, qui est la clause try-catch. Le bloc tryest entouré d'accolades et contient tout le code qui peut lancer des exceptions – dans ce cas, tout appel à newqui invoque des objets de type Framis. Immédiatement après le bloc tryse trouvent une ou plusieurs clauses catch, chacune d'entre elles spécifiant le type d'exception qu'elles capturent. Dans ce cas, catch(bad_alloc)signifie que les exceptions bad_allocseront interceptées ici. Cette clause catchparticulière est exécutée uniquement quand une exception bad_allocest lancée, et l'exécution continue à la fin de la dernière clause catchdu groupe (il n'y en a qu'une seule ici, mais il pourrait y en avoir plusieurs).
Dans cet exemple, on peut utiliser iostreams parce que les opérateurs operator new( )et delete( )globaux ne sont pas touchés.
operator delete( )suppose que l'adresse de Framisa été créée dans la réserve. C'est une supposition logique, parce que l' operator new( )local sera appelé à chaque fois que vous créez un objet Framisunique sur le tas – mais pas un tableau : l'opérateur newglobal est appelé pour les tableaux. Ainsi, l'utilisateur peut avoir accidentellement appelé l' operator delete( )sans utiliser la syntaxe avec les crochets vides pour indiquer la destruction d'un tableau. Ceci poserait un problème. Egalement, l'utilisateur pourrait detruire un pointeur vers un objet créé sur la pile. Si vous pensez que ces choses pourraient se produire, vous pourriez vouloir ajouter une ligne pour être sûr que l'adresse est dans la réserve et sur une frontière correcte (peut-être commencez-vous à voir le potentiel des newet deletesurchargés pour trouver les fuites de mémoire).
L' operator delete( )calcule le bloc de la réserve que représente ce pointeur, puis fixe l'indicateur de la carte d'allocation de ce bloc à faux pour indiquer que ce bloc a été libéré.
Dans main( ), suffisamment d'objets Framissont alloués dynamiquement pour manquer de mémoire ; ceci teste le comportement au manque de mémoire. Puis un des objets est libéré, et un autre est créé pour montrer que la mémoire libérée est réutilisée.
Puisque ce schéma d'allocation est spécifique aux objets Framis, il est probablement beaucoup plus rapide que le schéma d'allocation générique utilisé pour les newet deletepar défaut. Toutefois, vous devriez noter que cela ne marche pas automatiquement si l'héritage est utilisé (l'héritage est couvert au Chapitre 14).
13-5-3. Surcharger new & delete pour les tableaux▲
Si vous surchargez les opérateurs newet deletepour une classe, ces opérateurs sont appelés à chaque fois que vous créez un objet de cette classe. Toutefois, si vous créez un tableaud'objets de cette classe, l' operatornew( )global est appelé pour allouer suffisamment d'espace de stockage pour tout le tableau d'un coup, et l' operator delete( )global est appelé pour libérer ce stockage. Vous pouvez contrôler l'allocation de tableaux d'objets en surchargeant la version spéciale tableaux de operator new[ ]et operator delete[ ]pour la classe. Voici un exemple qui montre quand les deux versions différentes sont appelées :
//: C13:ArrayOperatorNew.cpp
// Operateur new pour tableaux
#include
<new>
// Définition de size_t
#include
<fstream>
using
namespace
std;
ofstream trace("ArrayOperatorNew.out"
);
class
Widget {
enum
{
sz =
10
}
;
int
i[sz];
public
:
Widget() {
trace <<
"*"
; }
~
Widget() {
trace <<
"~"
; }
void
*
operator
new
(size_t sz) {
trace <<
"Widget::new: "
<<
sz <<
" octets"
<<
endl;
return
::
new
char
[sz];
}
void
operator
delete
(void
*
p) {
trace <<
"Widget::delete"
<<
endl;
::
delete
[]p;
}
void
*
operator
new
[](size_t sz) {
trace <<
"Widget::new[]: "
<<
sz <<
" octets"
<<
endl;
return
::
new
char
[sz];
}
void
operator
delete
[](void
*
p) {
trace <<
"Widget::delete[]"
<<
endl;
::
delete
[]p;
}
}
;
int
main() {
trace <<
"new Widget"
<<
endl;
Widget*
w =
new
Widget;
trace <<
"
\n
delete Widget"
<<
endl;
delete
w;
trace <<
"
\n
new Widget[25]"
<<
endl;
Widget*
wa =
new
Widget[25
];
trace <<
"
\n
delete []Widget"
<<
endl;
delete
[]wa;
}
///
:~
Ici, les versions globales de newet deletesont appelées si bien que l'effet est le même que de ne pas avoir de version surchargée de newet deletesauf qu'une information de traçage est ajoutée. Bien sûr, vous pouvez utliser n'importe quel schéma d'allocation de mémoire dans les newet deletesurchargés.
Vous pouvez constater que la syntaxe de newet de deletepour tableaux est la même que pour leur version destinée aux objets individuels sauf qu'on y a ajouté des crochets. Dans les deux cas, on vous passe la taille de la mémoire que vous devez allouer. La taille passée à la version tableaux sera celle du tableau entier. Cela vaut la peine de garder à l'esprit que la seulechose que l' operator new( )surchargé soit obligé de faire est de renvoyer un pointeur vers un bloc mémoire suffisamment grand. Bien que vous puissiez faire une initialisation de cette mémoire, c'est normalement le travail du contructeur qui sera automatiquement appelé pour votre mémoire par le compilateur.
Le constructeur et le destructeur affichent simplement des caractères afin que vous puissiez voir quand ils ont été appelés. Voici à quoi ressemble le fichier de traces pour un compilateur :
new
Widget
Widget::
new
: 40
octets
*
delete
Widget
~
Widget::
delete
new
Widget[25
]
Widget::
new
[]: 1004
octets
*************************
delete
[]Widget
~~~~~~~~~~~~~~~~~~~~~~~~~
Widget::
delete
[]
Créer un objet individuel requiert 40 octet, comme vous pourriez prévevoir. (Cette machine utilise quatre octets pour un int.) L' operator new( )est appelé, puis le constructeur (indiqué par *). De manière complémentaire, appeler deleteentraîne l'appel du destructeur, puis de l' operator delete( ).
Comme promis, quand un tableau d'objets Widgetest créé, la version tableau de l' operator new( )est utilisée. Mais remarquez que la taille demandée dépasse de quatre octets la valeur attendue. Ces quatre octets supplémentaires sont l'endroit où le système conserve de l'information à propos du tableau, en particulier, le nombre d'objets dans le tableau. Ainsi, quand vous dites :
delete
[]Widget;
Les crochets disent au compilateur que c'est un tableau d'objets, afin que le compilateur génère le code pour chercher le nombre d'objets dans le tableau et appeler le destructeur autant de fois. Vous pouvez voir que même si les versions tableau de operator new( )et operator delete( )ne sont appelées qu'une fois pour tout le bloc du tableau, les constructeur et destructeur par défaut sont appelés pour chaque objet du tableau.
13-5-4. Appels au constructeur▲
En considérant que
MyType*
f =
new
MyType;
appelle newpour allouer un espace de stockage de taille adaptée à un objet MyType, puis invoque le constructeur de MyTypesur cet espace, que se passe-t-il si l'allocation de mémoire dans newéchoue ? Dans ce cas, le constructeur n'est pas appelé, aussi, bien que vous ayez un objet créé sans succès, au moins vous n'avez pas appelé le contructeur et vous ne lui avez pas passé un poiteur thisnul. Voici un exemple pour le prouver :
//: C13:NoMemory.cpp
// Le constructeur n'est pas appelé si new échoue
#include
<iostream>
#include
<new>
// définition de bad_alloc
using
namespace
std;
class
NoMemory {
public
:
NoMemory() {
cout <<
"NoMemory::NoMemory()"
<<
endl;
}
void
*
operator
new
(size_t sz) throw
(bad_alloc){
cout <<
"NoMemory::operator new"
<<
endl;
throw
bad_alloc(); // "Plus de memoire"
}
}
;
int
main() {
NoMemory*
nm =
0
;
try
{
nm =
new
NoMemory;
}
catch
(bad_alloc) {
cerr <<
"exception plus de mémoire"
<<
endl;
}
cout <<
"nm = "
<<
nm <<
endl;
}
///
:~
Quand le programme s'exécute, il n'affiche pas le message du constructeur, uniquement le message de l' operator new( )et le message dans le gestionnaire d'exception. Puisque newne retourne jamais, le constructeur n'est pas appelé si bien que son message n'est pas affiché.
Il est important que nmsoit initialisé à zéro parce que l'expression newn'aboutit jamais, et le pointeur devrait être à zéro pour être sûr que vous n'en ferez pas mauvais usage. Toutefois, vous devriez en fait faire plus de choses dans le gestionnaire d'exceptions que simplement afficher un message et continuer comme si l'objet avait été créé avec succès. Idéalement, vous ferez quelque chose qui permettra au programme de se remettre de ce problème, ou au moins de se terminer après avoir enregistré une erreur.
Dans les versions de C++ antérieures, c'était la pratique standard que newrenvoie zéro si l'allocation du stockage échouait. Cela évitait que la construction se produise. Toutefois, si vous essayez de faire renvoyer zéro à newavec un compilateur conforme au standard, il devrait vous dire que vous êtes supposés lancer une excpetion bad_allocà la place.
13-5-5. new & delete de placement▲
Il y a deux autres usages, moins courants, pour la surcharge de l' operator new( ).
- Vous pouvez avoir envie de placer un objet dans un emplacement spécifique de la mémoire. Ceci est particulièrement important pour les systèmes embarqués orientés matériel où un objet peut être synonyme d'un élément donnée du matériel.
- Vous pouvez vouloir être capable de choisir entre différents allocateurs quand vous appelez new.
Ces deux situations sont résolues par le même mécanisme : l' operator new( )surchargé peut prendre plus d'un argument. Comme vous l'avez vu auparavant, le premier argument est toujours la taille de l'objet, qui est calculée en secret et passée par le compilateur. Mais les autres arguments peuvent être tout ce que vous voulez – l'adresse où vous voulez que l'objet soit placé, une référence vers une fonction ou un objet d'allocation de mémoire, ou quoique ce soit de pratique pour vous.
La façon dont vous passez les arguments supplémentaires à operator new( )pendant un appel peut sembler un peu curieuse à première vue. Vous placez la liste d'arguments ( sansl'argument size_t, qui est géré par le compilateur) après le mot-clef newet avant le nom de la classe de l'objet que vous êtes en train de créer. Par exemple,
X*
xp =
new
(a) X;
Passera acomme deuxième argument à operator new( ). Bien sûr, ceci ne peut fonctionner que si un tel operator new( )a été déclaré.
Voic un exemple montrant comment vous pouvez placer un objet à un endroit donné :
//: C13:PlacementOperatorNew.cpp
// Placement avec l'opérateur new()
#include
<cstddef>
// Size_t
#include
<iostream>
using
namespace
std;
class
X {
int
i;
public
:
X(int
ii =
0
) : i(ii) {
cout <<
"this = "
<<
this
<<
endl;
}
~
X() {
cout <<
"X::~X(): "
<<
this
<<
endl;
}
void
*
operator
new
(size_t, void
*
loc) {
return
loc;
}
}
;
int
main() {
int
l[10
];
cout <<
"l = "
<<
l <<
endl;
X*
xp =
new
(l) X(47
); // X à l'emplacement l
xp->
X::
~
X(); // Appel explicite au destructeur
// Utilisé UNIQUEMENT avec le placement !
}
///
:~
Remarquez que operator newne renvoie que le pointeur qui lui est passé. Ainsi, l'appelant décide où l'objet va résider, et le contructeur est appelé pour cette espace mémoire comme élément de l'expression new.
Bien que cet exemple ne montre qu'un argument additionnel, il n'y a rien qui vous empêche d'en ajouter davantage si vous en avez besoin pour d'autres buts.
Un dilemne apparaît lorsque vous voulez détruire l'objet. Il n'y a qu'une version de operator delete, et il n'y a donc pas moyen de dire, “Utilise mon dé-allocateur spécial pour cet objet.” Vous voulez appeler le destructeur, mais vous ne voulez pas que la mémoire soit libérée le mécanisme de mémoire dynamique parce qu'il n'a pas été alloué sur la pile.
La réponse est une syntaxe très spéciale. Vous pouvez explicitement appeler le destructeur, comme dans
xp->
X::
~
X(); // Appel explicite au destructeur
Un avertissement sévère est justifié ici. Certaines personnes voient cela comme un moyen de détruire des objets à un certain moment avant la fin de la portée, plutôt que soit d'ajuster la portée ou (manière plus correcte de procéder) en utilisant la création dynamique d'objets s'ils veulent que la durée de vie de l'objet soit déterminée à l'exécution. Vous aurez de sérieux problèmes si vous appelez le destructeur ainsi pour un objet ordinaire créé sur la pile parce que le destructeur sera appelé à nouveau à la fin de la portée. Si vous appelez le destrcuteur ainsi pour un objet qui a été créé sur le tas, le destructeur s'exécutera, mais la mémoire ne sera pas libérée, ce qui n'est probablement pas ce que vous désirez. La seule raison pour laquelle le destructeur peut être appelé explicitement ainsi est pour soutenir la syntaxe de placement de operator new.
Il y a également un operator deletede placement qui est appelé uniquement si un constructeur pour une expression de placement newlance une exception (afin que la mémoire soit automatiquement nettoyée pendant l'exception). L' operator deletede placement a une liste d'arguments qui correspond à l' operator newde placement qui est appelé avant que le constucteur ne lance l'exception. Ce sujet sera couvert dans le chapitre de gestion des exceptions dans le Volume 2.
13-6. Résumé▲
Il est commode et optimal du point de vue de l'efficacité de créer des objets sur la pile, mais pour résoudre le problème de programmation général vous devez être capables de créer et de détruire des objets à tout moment durant l'exécution d'un programme, spécialement pour réagir à des informations provenant de l'extérieur du programme. Bien que l'allocation dynamique de mémoire du C obtienne du stockage sur le tas, cela ne fournit pas la facilité d'utilisation ni la garantie de construction nécessaire en C++. En portant la création dynamique d'objets au coeur du langage avec newet delete, vous pouvez créer des objets sur le tas aussi facilement que sur la pile. En outre, vous profitez d'une grande flexibilité. Vous pouvez modifier le comportement de newet deletes'ils ne correspondent pas à vos besoins, particulièrement s'ils ne sont pas suffisamment efficaces. Vous pouvez aussi modifier ce qui se produit quand l'espace de stockage du tas s'épuise.
13-7. Exercices▲
Les solutions aux exercices choisis peuvent être trouvées dans le document électronique The Thinking in C++ Annotated Solution Guide, disponible à un coût modeste sur www.BruceEckel.com.
- Créez une classe Countedqui contient un int idet un static int count. Le constructeur par défaut devrait commencer ainsi : Counted( ) : id(count++) {. Il devrait aussi afficher son idet qu'il est en train d'être créé. Le destructeur devrait afficher qu'il est en train d'être détruit et son id. Testez votre classe.
- Prouvez vous à vous-même que newet deleteappellent toujours les constructeurs et les destructeurs en créant un objet de la class Counted(de l'exercice 1) avec newet en le détruisant avec delete. Créez et détruisez également un tableau de ces objets sur le tas.
- Créez un objet PStashet remplissez le avec de nouveaux objets de l'exercice 1. Observez ce qui se passe quand cet objet PStashdisparaît de la portée et que son destructeur est appelé.
- Créez un vector<Counted*>et remplissez le avec des pointeurs vers des nouveaux objets Counted(de l'exercice 1). Parcourez le vectoret affichez les objets Counted, ensuite parcourez à nouveau le vectoret detruisez chacun d'eux.
- Répétez l'exercice 4, mais ajoutez une fonction membre f( )à Countedqui affiche un message. Parcourez le vectoret appelez f( )pour chaque objet.
- Répétez l'exercice 5 en utilisant un PStash.
- Répétez l'exercice 5 en utilisant Stack4.hdu chapitre 9.
- Créez dynamiquement un tableau d'objets class Counted(de l'exercice 1). Appelez deletepour le pointeur résultant, sans les crochets. Expliquez les résultats.
- Créez un objet de class Counted(de l'exercice 1) en utilisant new, transtypez le pointeur résultant en un void*, et détruisez celui là. Expliquez les résultats.
- Executez NewHandler.cppsur votre machine pour voir le compte résultant. Calculez le montant d'espace libre (free store ndt) disponible pourvotre programme.
- Créez une classe avec des opérateurs newet deletesurchargés, à la fois les versions 'objet-unique' et les versions 'tableau'. Démontrez que les deux versions fonctionnent.
- Concevez un test pour Framis.cpppour vous montrer approximativement à quel point les versions personnalisées de newet deletefonctionnent plus vite que les newet deleteglobaux.
- Modifiez NoMemory.cppde façon qu'il contienne un tableau de intet de façon qu'il alloue effectivement de la mémoire au lieu de lever l'exception bad_alloc. Dans main( ), mettez en place une boucle whilecomme celle dans NewHandler.cpppour provoquer un épuisement de la mémoire and voyez ce qui se passe si votre operator newne teste pas pour voir si la mémoire est allouée avec succès. Ajoutez ensuite la vérification à votre operator newet jetez bad_alloc.
- Créez une classe avec un newde placement avec un second argument de type string. La classe devrait contenir un static vector<string>où le second argument de newest stocké. The newde placement devrait allouer de l'espace comme d'habitude. Dans main( ), faites des appels à votre newde placement avec des arguments stringqui décrivent les appels (Vous pouvez vouloir utiliser les macros du préprocesseur __FILE__et __LINE__).
- Modifiez ArrayOperatorNew.cppen ajoutant un static vector<Widget*>qui ajoute chaque adresse Widgetqui est allouée dans operator new( )et l'enlève quand il est libéré via l' operator delete( ). (Vous pourriez avoir besoin de chercher de l'information sur vectordans la documentation de votre Librairie Standard C++ ou dans le second volume de ce livre, disponible sur le site Web.) Créez une seconde classe appelée MemoryCheckerayant un destructeur qui affiche le nombre de pointeurs Widgetdans votre vector. Créez un programme avec une unique instance globale de MemoryCheckeret dans main( ), allouez dynamiquement et détruisez plusieurs objets et tableaux de Widget. Montrez que MemoryCheckerrévèle des fuites de mémoire.