TD 07

Transcription

TD 07
Centre Informatique pour les Lettres
et les Sciences Humaines
C++ : WinTD 07
Expériences avec new et delete
1 - De quoi s'agit-il ?...............................................................................................................2
Choix techniques...................................................................................................... 2
2 - Création du projet .............................................................................................................2
3 - Dessin de l'interface ..........................................................................................................2
4 - Ecriture du code................................................................................................................3
La
La
La
La
fonction
fonction
fonction
fonction
CMonDialogue::OnInitDialog() .......................................................... 4
CMonDialogue::affiche() ................................................................... 4
CMonDialogue::OnButtonNew() ............................................................ 6
CMonDialogue::OnButtonDelete() ...................................................... 7
5 - Séquences d'actions "anormales" .......................................................................................8
Utiliser delete avant d'avoir utilisé new.................................................................... 8
Utiliser new deux fois de suite ................................................................................... 9
Utiliser delete deux fois de suite ............................................................................. 9
6 - Qu'avons nous appris ? ...................................................................................................10
7 - Exercices.........................................................................................................................11
Document du 20/09/02 - Retrouvez la version la plus récente sur http://www.up.univ-mrs.fr/wcpp/wintd.htm
C++ - WinTD 07
Expériences avec new et delete
2/11
Le programme réalisé au cours du TD 07 n'effectue
aucun traitement présentant le moindre intérêt
concret. Sa mise au point et son utilisation vont
cependant nous permettre d'examiner plus en détail
les conséquences entraînées par la mise en pratique
des techniques présentées dans la Leçon 7 :
variables locales statiques, allocation dynamique et
libération de la mémoire.
Nous nous attacherons particulièrement aux
symptômes présentés par les programmes contenant
les erreurs les plus fréquemment commises par les
débutants. Il est en effet important que vous
appreniez à reconnaître ces symptômes et soyez en
mesure de les interpréter comme des indices
pointant vers votre erreur.
L'interface utilisateur du programme réalisé au
cours du TD 7
1 - De quoi s'agit-il ?
Au cœur du TD 07 se trouve une variable de type "pointeur sur int". Les deux boutons
intitulés "new" et "delete" permettent respectivement de réserver une zone de mémoire (dont
l'adresse sera stockée dans le pointeur) et de libérer la zone de mémoire dont l'adresse est
contenue dans le pointeur. La partie supérieure du dialogue permet de visualiser l'état actuel
du pointeur et de la zone qu'il désigne, alors que la partie inférieure est occupée par une
EditBox permettant de garder une trace des opérations effectuées.
Le déclenchement "manuel" des opérations new et delete nous permettra, dès que notre
programme sera en état de fonctionner, d'expérimenter les effets de certaines séquences
d'actions habituellement effectuées par des programmes. Notre dialogue s'efforce, en fait, de
présenter à l'écran ce que tout programmeur devrait avoir présent à l'esprit lorsqu'il utilise
l'allocation dynamique. Ce genre de détails est, bien entendu, normalement caché à
l'utilisateur d'un programme "ordinaire".
Choix techniques
Etant donné que les fonctions appelées par les boutons "new" et "delete" doivent toutes deux
avoir accès à notre variable de type "pointeur sur int" et que, par ailleurs, ces fonctions sont
nécessairement dépourvues de paramètres, il est pratiquement obligatoire que cette variable
soit une variable membre de la classe correspondant à notre dialogue.
Les divers affichages peuvent tous être réalisés en utilisant une méthode que nous
connaissons maintenant bien : une variable CString associée à une EditBox en mode "read
only". Nous apporterons un petit raffinement supplémentaire à notre interface en assortissant
ces affichages de quelques explications destinées à aider l'utilisateur du programme à se
souvenir de la nature des informations présentées. Ces explications seront insérées dans le
dessin de l'interface par le biais de contrôles de type "Static Text".
2 - Création du projet
Créez, comme dans les TD précédents, un projet d'application basée sur un dialogue. La suite
du texte suppose que ce projet s'appelle TD 07 et que vous avez renommé la classe de dialogue
CMonDialogue et les fichiers correspondants monDialogue.cpp et monDialogue.h (cf. TD 01).
3 - Dessin de l'interface
Les contrôles "Static Text" destinés à rendre le dialogue plus intelligible sont mis en place en
sélectionnant, dans la barre d'outil "Controls", l'icône
. La seule propriété intéressante dans
J-L Péris - 20/09/02
C++ - WinTD 07
Expériences avec new et delete
3/11
le cas de ce type de contrôle est leur "Caption", qui constitue en fait le texte qui sera présent
dans la boîte de dialogue.
Les trois Static Text qui nous sont nécessaires ont respectivement pour "caption" : "La variable
m_lePointeur contient :", "à cette adresse, on trouve la valeur :" et "m_lePointeur est :". Pour
obtenir une mise en page correcte, il faut ajuster la taille de chacun des contrôles à la longueur
du texte qu'il doit afficher, ce qui permettra de placer les EditBox correspondantes assez près
du commentaire les concernant (ce qui est indispensable pour obtenir l'effet explicatif
recherché).
Contrairement à ce qui se passe dans le cas des autres contrôles, Visual C++ n'attribue pas
automatiquement des ID différentes aux différents Static Text présents dans un même
dialogue. L'ID qui est systématiquement utilisée est IDC_STATIC, et elle a la propriété d'être
invisible au ClassWizard. Ceci signifie que, s'il vous arrive de vouloir associer une variable
membre à un contrôle Static Text, il vous faudra tout d'abord changer son ID pour lui en
donner une qui lui soit propre1.
Les trois EditBox de la partie supérieure du dialogue ont des propriétés semblables, à
l'exception, bien entendu de leurs ID (IDC_ADRESSE, IDC_DEREF et IDC_VALIDE) :
Dans la page
Styles
la propriété
Align Text
Read only
doit être
centered
cochée
L'EditBox de la partie inférieure est un peu différente :
Dans la page
General
Styles
la propriété
ID
Multiline
Auto HScroll
Want return
Read only
Vertical scroll
doit être
IDC_HISTORIQUE
cochée
non cochée
cochée
cochée
cochée
Les deux boutons n'ont rien de particulier, leurs ID sont IDC_BUTTON_NEW et
IDC_BUTTON_DELETE. Nous savons qu'une fonction devra être associée à chacun d'entre eux, et
nous pouvons dès maintenant créer ces deux fonctions, en acceptant les noms suggérés par le
ClassWizard. Il faut également créer (à l'aide du ClassWizard) les variables membre associées
aux contrôles dont notre programme aura à modifier le contenu. Il s'agit des quatre EditBox, et
chacune d'entre-elles peut être associée à une variable de type CString.
ID du contrôle
IDC_ADRESSE
IDC_DEREF
IDC_VALIDE
IDC_HISTORIQUE
Catégorie
Value
Value
Value
Value
Variable
Type
Nom
CString
m_afficheAdresse
CString
m_afficheDeref
CString
m_afficheValide
CString
m_historique
4 - Ecriture du code
La première chose à faire est d'ajouter2 à la classe CMonDialogue une variable membre de type
"pointeur sur int" que nous nommerons m_lePointeur.
Nous savons qu'un pointeur sur int qui ne contient pas l'adresse d'une zone de mémoire
stockant une valeur de type int doit être considéré comme invalide. L'une des EditBox sert
précisément à indiquer la validité actuelle du pointeur, mais cette information ne peut être
déduite ni du simple examen du pointeur, ni de l'examen de la zone de mémoire pointée. En
1 La raison essentielle pour laquelle on associe des variables aux contrôles est un besoin de consultation ou de
modification par le programme de l'information affichée à l'écran. La vocation première d'un Static Text est l'affichage
d'un message choisi lors de la conception du dialogue (et donc parfaitement connu du programmeur) et restant
immuable pendant l'exécution du programme. Il est donc assez logique que, par défaut, Visual C++ ne propose pas
d'associer de variable membre à un contrôle de ce type.
2 En cliquant avec le bouton droit sur le nom de la classe dans l'onglet ClassView de la fenêtre Workspace, et en
choisissant "Add Member Variable" dans le menu local qui apparaît alors.
J-L Péris - 20/09/02
C++ - WinTD 07
Expériences avec new et delete
4/11
effet, rien ne distingue a priori l'adresse d'une zone de mémoire contenant un int de l'adresse
d'une zone de mémoire contenant autre chose, et nous savons (cf. Leçon 1) qu'il est toujours
possible d'interpréter la zone de mémoire en question comme si elle représentait un int. Il est
donc nécessaire d'utiliser une autre variable pour indiquer si le pointeur est ou non valide.
L'état de cette variable devra être tenu à jour, en fonction des opérations affectant la validité du
pointeur qui seront effectuées. Cette variable étant destinée à dire si le pointeur est ou non
valide, elle sera naturellement de type bool. Comme toutes les fonctions agissant sur
m_lePointeur doivent également agir sur ce booléen, c'est une variable membre de la classe
CMonDialogue qu'il nous faut créer2, et nous l'appellerons m_valide.
Une fois ces variables créées, nous pouvons ajouter à la classe CMonDialogue3 la fonction
membre (nommée affiche(), de type void et n'acceptant aucun paramètre) qui assurera la
mise à jour de l'affichage des informations concernant le pointeur.
Cette fonction doit être exécutée dès le lancement du programme, et il convient donc de
l'appeler à partir de la fonction OnInitDialog().
La fonction CMonDialogue::OnInitDialog()
Cette fonction fournit également une occasion d'attribuer à la variable m_valide une valeur
signalant que m_lePointeur n'est pas, au début de l'exécution du programme, un pointeur
valide.
1
2
3
4
5
6
7
8
9
BOOL CMonDialogue::OnInitDialog()
{
CDialog::OnInitDialog();
// Set the icon for this dialog. The framework does this automatically
// when the application's main window is not a dialog
SetIcon(m_hIcon, TRUE);
// Set big icon
SetIcon(m_hIcon, FALSE);
// Set small icon
// TODO: Add extra initialization here
m_valide = false;
affiche();
return TRUE; // return TRUE unless you set the focus to a control
}
Contrairement à ce que suggère le commentaire introduit par Visual C++ dans la fonction
OnInitDialog(), ce ne sont pas réellement à des initialisations que nous procédons ici. La
véritable initialisation de variables membre est une opération que nous ne savons pas encore
effectuer, et nous devons nous contenter ici d'affecter une première valeur aux variables.
Etant donné que la fonction OnInitDialog() est automatiquement exécutée dès la création
du dialogue, ces affectations sont, du point de vue des autres fonctions membres,
fonctionnellement équivalentes à des initialisations : les variables membre prennent leur
valeur "initiale" avant que quiconque n'ait eu l'occasion de les utiliser.
La fonction CMonDialogue::affiche()
Le rôle de cette fonction est de mettre à jour les variables m_afficheAdresse,
m_afficheValide et m_afficheDeref. La première de ces chaînes doit indiquer la valeur
contenue dans m_lePointeur. Elle peut être construite à l'aide de la fonction Format(),
exactement comme si la variable qui nous intéresse était d'un type entier ordinaire :
m_afficheAdresse.Format("%i", m_lePointeur);
La coutume veut que les adresses soient exprimées en hexadécimal plutôt qu'en décimal et la
fonction Format() nous permet de respecter cette tradition sans avoir à fournir un effort trop
important : le spécificateur de format "%X" permet d'obtenir la représentation hexadécimale
d'un nombre entier. Nous afficherons donc cette valeur dans les deux bases, en faisant
précéder la représentation hexadécimale du préfixe "0x" qui annonce, en C et C++, l'apparition
d'une telle représentation4 :
3 En cliquant avec le bouton droit sur le nom de la classe dans l'onglet ClassView de la fenêtre Workspace, et en
choisissant "Add Member Function" dans le menu local qui apparaît alors.
4 Etant donné la nature du programme que nous sommes en train d'écrire, il y a peu de risques que ses utilisateurs
ignorent les conventions en vigueur en C++...
J-L Péris - 20/09/02
C++ - WinTD 07
Expériences avec new et delete
5/11
m_afficheAdresse.Format("%i (0x%X)", m_lePointeur, m_lePointeur);
La valeur de m_lePointeur est représentée deux fois (en décimal, puis en hexadécimal), ce
qui explique que le nom de la variable soit mentionné deux fois dans la série de paramètres
transmis à la fonction Format().
La variable m_afficheValide n'a, quant à elle, que deux valeurs possibles qui doivent
correspondre directement à celles du booléen m_valide. L'opérateur ternaire permet d'établir
très simplement cette correspondance :
m_afficheValide = (m_valide) ? "Valide" : "Invalide";
La variable m_afficheDeref pose un problème un peu plus délicat, car pour afficher le
contenu de la zone de mémoire dont l'adresse est dans le pointeur, il faut accéder à cette zone.
Or, lorsque le pointeur n'est pas valide, il est tout à fait possible que cet accès ne soit pas
autorisé : si l'adresse utilisée se trouve en dehors de la zone de mémoire attribuée par Windows
à notre application, la sanction sera immédiate.
Illustrons cette situation en lançant l'exécution du programme après avoir défini la fonction
affiche() de la façon suivante :
1
2
3
4
5
6
7
void CMonDialogue::affiche()
{
//affichage du contenu du pointeur
m_afficheAdresse.Format("%i (0x%X)", m_lePointeur, m_lePointeur);
//affichage de sa validité
m_afficheValide = (m_valide) ? "Valide" : "Invalide";
//et si on le déréférençait ?
m_afficheDeref.Format("%i",*m_lePointeur);
UpdateData(FALSE);//les variables associées aux contrôles ont été affectées
}
Le résultat de cette exécution est l'affichage d'une fenêtre tristement célèbre :
En effet, la variable m_lePointeur n'a jamais
reçu de valeur significative. Au moment où
elle est déréférencée, ce sont donc des cases
mémoires dont l'état est normalement
imprévisible qui se trouvent interprétées
comme représentant une adresse.
En fait, dans le contexte actuel du TD 07, ce n'est pas tout à fait un état imprévisible des
cases mémoires qui intervient. Pendant la phase de mise au point des programmes, Visual
C++ prend soin de placer les cases mémoires attribuées à des pointeurs dans un état qui
garantit que, si le pointeur est déréférencé avant d'avoir reçu une valeur légitime, la violation
d'accès aura effectivement lieu. Ceci vous garantit la détection immédiate de cette grossière
erreur de programmation. Dans d'autres contextes (et, en particulier, si vous travaillez avec
un compilateur moins "maternant"), il est fort possible que la valeur imprévisible de
m_lePointeur se trouve, par hasard, ne pas provoquer de violation d'accès pendant que vous
testez votre programme (c'est seulement lorsque votre vie, votre fortune et votre réputation
dépendront de la bonne exécution du programme que l'erreur se révélera).
Comment peut-on régler ce problème ? Nous pourrions nous baser sur la valeur contenue dans
m_valide, et n'afficher le contenu de la zone mémoire pointée que lorsque le pointeur est
valide. Toutefois, l'un des objectifs de ce TD est d'illustrer le fait que l'opérateur delete n’a
d’effet prévisible ni sur l'état du pointeur qui lui désigne la zone à libérer, ni sur l'état de la
zone en question. Notre programme va donc, à titre tout à fait exceptionnel, déréférencer
délibérément m_lePointeur après que celui-ci ait cessé d'être valide. Il nous faut donc nous
baser sur un autre critère que son invalidité pour éviter de déréférencer m_lePointeur avant
qu'il ait reçu une première valeur le rendant valide. On signale habituellement qu'un pointeur
n'est pas en état d'être déréférencé en le rendant NULL. Ajoutons à la fonction OnInitDialog()
une instruction donnant cette valeur au pointeur :
// TODO: Add extra initialization here
m_lePointeur = NULL;
m_valide = false;
affiche();
J-L Péris - 20/09/02
C++ - WinTD 07
Expériences avec new et delete
La fonction affiche() peut alors simplement
m_lePointeur lorsque celui-ci est NULL :
s'abstenir
6/11
de
tenter
de déréférencer
3
void CMonDialogue::affiche()
{
//affichage du contenu du pointeur
m_afficheAdresse.Format("%i (0x%X)", m_lePointeur, m_lePointeur);
4
//affichage de sa validité
m_afficheValide = (m_valide) ? "Valide" : "Invalide";
5
6
7
8
//et si on le déréférençait ?
if(m_lePointeur == NULL)
m_afficheDeref = "m_lePointeur est NULL";
else
m_afficheDeref.Format("%i",*m_lePointeur);
1
2
9
10
UpdateData(FALSE);
}
La fonction CMonDialogue::OnButtonNew()
La tâche principale dévolue à la fonction OnButtonNew() est de réserver une zone de mémoire
pouvant stocker une valeur de type int. Il ne faut surtout jamais oublier que cette réservation
est une opération effectuée pendant l'exécution du programme (et non pendant la compilation
du texte source...) et qu'elle est toujours susceptible d'échouer. En conséquence, il faut
systématiquement tester les adresses obtenues par l'intermédiaire de l'opérateur new. Le reste
de la fonction n'est destiné qu'a tenir à jour l'historique des opérations effectuées qui est
affiché dans la partie inférieure du dialogue (attention, toutefois, à ne pas oublier de mettre à
jour la variable m_valide et d'appeler affiche() pour que la description de l'état actuel du
pointeur soit mise à jour).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void CMonDialogue::OnButtonNew()
{
m_lePointeur = new int;
if (m_lePointeur != NULL)
{
CString texteAdresse;
texteAdresse.Format("%i (0x%X)\r\n", m_lePointeur, m_lePointeur);
m_historique = m_historique + "===== new =====\r\nAllocation d'une zone de"
"mémoire pouvant stocker une valeur de type int. Son adresse est ";
m_historique = m_historique + texteAdresse;
m_valide = true;
}
else
{
m_valide = false;
m_historique = m_historique + "Echec d'allocation !\r\n";
}
affiche();
}
L'exécution du programme se déroule alors de la façon suivante :
Avant de cliquer sur new
Après avoir cliqué sur new
J-L Péris - 20/09/02
C++ - WinTD 07
Expériences avec new et delete
7/11
Si l'affichage initial n'appelle guère de commentaires, le résultat obtenu lorsqu'on clique sur le
bouton new est plus intéressant. Notons tout d'abord que, si l'adresse de la zone de mémoire
réservée peut être différente dans votre cas, son "contenu" devrait toujours être identique.
En effet, pour nous aider à repérer les zones de mémoire allouées dynamiquement et n'ayant
jamais reçu de valeurs significatives, Visual C++ les place dans un état particulier : toutes les
cases mémoire reçoivent la valeur 0xCD, ce qui, dans le cas d'un entier occupant 4 octets,
correspond à la valeur -842150451. Comme dans le cas (évoqué plus haut) de l'initialisation
des zones mémoires correspondant à des pointeurs, cette mesure préventive n'est pas une
caractéristique du langage C++, mais un confort proposé par le compilateur Microsoft. Dans
d'autres environnements de développement, ou dans d'autres contextes, ce type de protection
peut ne pas être disponible.
L'aspect le plus intéressant de l'exécution de la version actuelle de notre programme apparaît
lorsqu'elle s'achève. Lorsque vous refermez le dialogue, le retour à l'environnement Visual C++
s'accompagne en effet de l'apparition d'un message dans l'onglet Debug de la fenêtre Output5 :
Detected memory leaks!
Dumping objects ->
c:\MES DOCUMENTS\TD 07\MonDialogue.cpp(121) : {64} normal block at
0x00771CB0, 4 bytes long.
Data: <
> CD CD CD CD
Object dump complete.
The thread 0xFFC4E243 has exited with code 2 (0x2).
The program 'c:\MES DOCUMENTS\TD 07\Debug\TD 07.exe' has exited with code 2
(0x2).
Ce message signale sans ambiguïté que notre programme présente une "fuite de mémoire",
c'est à dire qu'il ne libère pas toutes les zones qui ont été réservées à l'aide de new6. Pour nous
faciliter la découverte de l'origine de ce problème, Visual C++ nous indique le nom du fichier et
le numéro de la ligne où figure l'utilisation de new qui est en cause. L'adresse et la taille de la
zone de mémoire non libérée sont également indiquées, de même que le contenu de cette zone
au moment de la fin du programme.
Si plusieurs zones de mémoire restent non libérées à la fin du programme, la fenêtre Output
donnera tous ces renseignements pour chacune d'entre-elles. Dans certains cas, le message
ainsi généré atteint une longueur considérable, au point que le temps nécessaire à votre
ordinateur pour le composer devienne clairement perceptible...
Dans notre cas, il est facile de vérifier que les renseignements figurant dans ce message sont
conformes à ceux qui figuraient dans la boîte de dialogue avant que nous la fermions.
La fonction CMonDialogue::OnButtonDelete()
Pour pouvoir éviter cette fuite de mémoire, il nous faut évidemment écrire la fonction dont la
mission est de libérer le bloc de mémoire dont l'adresse est dans m_lePointeur ! Cette fonction
n'est pas très complexe : outre les commentaires destinés à la fenêtre retraçant l'historique des
actions effectuées, il faut simplement veiller à mettre à jour la variable m_valide et à appeler
affiche() pour que la description de l'état actuel du pointeur soit mise à jour :
1
2
3
4
5
6
7
8
9
10
11
void CMonDialogue::OnButtonDelete()
{
CString texteAdresse;
texteAdresse.Format("%i (0x%X)\r\n", m_lePointeur, m_lePointeur);
m_historique = m_historique + "===== delete =====\r\n"
"Libération de la zone de mémoire située à l'adresse ";
m_historique = m_historique + texteAdresse;
delete m_lePointeur;
m_valide = false;
affiche();
}
Si la fenêtre Output n'est pas visible, vous pouvez la faire apparaître en cliquant sur l'icône
de la barre d'outils.
Rassurez-vous, lorsque l'exécution du programme s'achève, toute la mémoire que ce programme utilisait est de toutes
façons restituée au système. Le problème posé par les fuites de mémoire est qu'elles indiquent un défaut de conception
du programme, et donc un risque que celui-ci ne fonctionne pas correctement.
5
6
J-L Péris - 20/09/02
C++ - WinTD 07
Expériences avec new et delete
8/11
Une fois cette fonction disponible, l'exécution du programme peut, si chaque clic sur le bouton
"new" est suivi d'un clic sur le bouton "delete", ne donner lieu à aucune fuite de mémoire. Ce
serait le cas, par exemple, dans la situation illustrée ci dessous.
Sur cet affichage, on peut remarquer que
chaque delete s'applique effectivement sur
l'adresse qui a été placée dans la variable
m_lePointeur lors du new précédent.
Une des choses les plus importantes à
souligner à propos de l'utilisation de delete
est le fait que la valeur contenue dans le
pointeur n'est en aucune façon modifiée.
Dans le dialogue représenté ci-contre,
l'historique indique que delete vient d'être
appliqué
à
m_lePointeur
qui,
en
conséquence, est devenu invalide.
Le contenu de la variable m_lePointeur n'a
cependant pas changé : il s'agit toujours de
l'adresse de la zone de mémoire réservée lors
du dernier appel à new, qui vient, comme le
confirme l'historique, d'être libérée.
Un usage équilibré de new et delete : si l'on met fin à
l'exécution du programme, aucune fuite de mémoire ne
sera signalée.
Le comportement du programme est tout à fait conforme à ce que nous avions prévu, mais
l'absence de fuite de mémoire n'est obtenue que tant que l'utilisateur respecte une certaine
logique dans son usage des boutons. Il est en effet nécessaire de commencer par un new et
d'alterner sans erreur les appels à new et à delete. Cette règle offre trois possibilités de
transgression :
- utiliser delete alors qu'aucun new n'a été effectué ;
- utiliser new deux fois de suite ;
- utiliser delete deux fois de suite.
5 - Séquences d'actions "anormales"
Nous allons provoquer délibérément les trois types de situations évoquées ci-dessus, en
essayant de bien comprendre les symptômes qui peuvent apparaître dans chacun des cas.
Utiliser delete avant d'avoir utilisé new
Lorsqu'un pointeur n'a jamais reçu de contenu correspondant à l'adresse d'une zone de
mémoire destinée à stocker une valeur du type correspondant à celui du pointeur, ce pointeur
est invalide. Il existe en fait deux façons pour un pointeur d'être invalide : le pointeur peut être
NULL, ou il peut contenir une adresse quelconque.
Utiliser delete sur un pointeur NULL est sans danger.
La norme internationale définissant le langage C++ exige explicitement que cette opération
n'ait pas de conséquences fâcheuses. D'un certain point de vue, on peut considérer qu'il
s'agit là d'une raison supplémentaire pour rendre systématiquement NULL les pointeurs
invalides, mais l'idée que l'on puisse s'appuyer sur cette tolérance pour s'épargner l'effort de
gérer correctement la mémoire n'est pas des plus plaisantes : un programme écrit de cette
façon comporte plus que probablement d'autres preuves de l'inconséquence de son auteur, et
mieux vaut s'en tenir à l'écart.
Si le pointeur sur lequel on applique delete n'est pas NULL, mais contient une adresse qui
n'est pas celle d'un bloc de mémoire réservé par l'intermédiaire de new, le résultat est le même
que lorsque l'adresse est celle d'un bloc qui a déjà été libéré. Nous examinerons donc ce cas
dans la rubrique "Utiliser delete deux fois de suite".
Dans le cas de notre programme, étant donné que la fonction OnInitDialog() rend
m_lePointeur NULL avant que l'utilisateur ait l'occasion de cliquer sur le moindre bouton,
cliquer d'abord sur le bouton delete ne provoque aucun événement particulier.
J-L Péris - 20/09/02
C++ - WinTD 07
Expériences avec new et delete
9/11
Utiliser new deux fois de suite
Si une seconde réservation de mémoire
intervient avant que le premier bloc ait été
libéré, un problème se pose : l'adresse du
premier bloc, qui n'est stockée que dans
m_lePointeur, va y être remplacée par celle
du second bloc.
Dans la situation illustrée ci-contre, le
programme a perdu toute trace de l'adresse
du premier bloc (0x300030) et, quoi que
fasse l'utilisateur avant de mettre fin à
l'exécution, la fuite de mémoire est
inévitable !
Il s'agit là d'une situation tout à fait classique. Imaginez que la fonction qui pratique cette
allocation qui ne s'accompagne d'aucune libération soit appelée à l'intérieur d'une boucle...
Même s'il ne s'agit que de quatre octets à chaque fois, la mémoire disponible risque fort de finir
par être épuisée.
Les désagréments commencent malheureusement bien avant que la mémoire soit épuisée.
L'appropriation d'une grande quantité de mémoire par notre programme (qui, dans ce cas, ne
l'utilise même pas !) se traduit inévitablement par un ralentissement du système, au
détriment des autres programmes qui essaient de fonctionner en même temps.
Utiliser delete deux fois de suite
Que se passe-t-il si delete est appliqué à un pointeur qui ne contient ni la valeur NULL ni
l'adresse d'un bloc de mémoire actuellement réservé à la suite d'une utilisation de new ?
La réponse officielle à cette question est claire : il peut se passer absolument n'importe quoi.
Ce n'importe quoi inclut évidemment l'hypothèse la plus défavorable, à savoir qu'il ne se passe
rien de vraiment notable au moment où cette opération coupable est effectuée. Bien entendu,
les conséquences seront d'autant plus douloureuses qu'elles seront tardives, et l'erreur peut
s'avérer extrêmement difficile à repérer et à corriger.
Appliquer delete à un pointeur non NULL ne contenant pas l'adresse d'un bloc de mémoire
légitimement libérable est très dangereux.
Remarque : le fait qu'un pointeur soit valide ne signifie pas nécessairement que le bloc de
mémoire dont il contient l'adresse soit "légitimement libérable". Un pointeur qui contient
l'adresse d'une variable du bon type, par exemple, est tout à fait valide. La zone de mémoire
attribuée à la variable n'ayant pas été réservée par new, il est toutefois formellement
déconseillé d'essayer de la libérer en appliquant delete au pointeur !
Heureusement, Visual C++ propose une réponse un peu différente de la réponse officielle. Dans
la phase de mise au point d'un programme, appliquer delete à un pointeur qui ne contient pas
une valeur admissible provoque une interruption de l'exécution :
Dans cette situation, cliquer sur le bouton delete...
...provoque l'apparition de ce message.
J-L Péris - 20/09/02
C++ - WinTD 07
Expériences avec new et delete
10/11
Même s'il faut bien reconnaître que ce message n'est pas forcément très clair pour un débutant
(surtout si celui-ci est francophone), l'interruption de l'exécution constitue déjà, en elle-même,
une information précieuse.
Pour un programmeur un peu plus aguerri, les mots "dbgheap" et BLOCK_TYPE_IS_VALID
apparaissant sous le titre "Debug Assertion Failed!" disent tout ce qu'il y a à dire en la
circonstance : le contrôle de la gestion du tas7 a détecté une opération portant sur un bloc qui
n'est pas en état de la supporter. Comme delete est la seule opération que nous puissions
appliquer à un bloc de mémoire dans ce contexte, la conclusion s'impose : quelqu'un essaie de
libérer un bloc qui n'est pas (ou plus) réservé !
Si l'on clique sur le bouton Réessayer, Visual C++ ouvre le fichier qui contient la ligne de code
qui est à l'origine de l'apparition du message. Bien entendu, il s'agit du code qui a détecté
l'erreur (c'est à dire d'une portion de la librairie Microsoft) et non du code qui a commis cette
erreur. Pour remonter à l'origine du problème (c'est à dire, très vraisemblablement, à une
fonction que nous avons écrite nous-mêmes...), il nous faut franchir une étape supplémentaire.
L'exécution du programme est suspendue,
et nous disposons d'un moyen nous
permettant d'explorer la séquence d'appels
qui l'a conduit à cet arrêt : la fenêtre "Call
Stack"8
Comme son nom l'indique (Call = appel,
Stack = pile, au sens d'une pile d'assiettes,
et non d'une pile électrique), cette fenêtre
propose une représentation de l'empilement
des appels successifs, ce qui signifie que :
La fenêtre "Call Stack"
- la fonction en cours d'exécution au moment de l'interruption du programme se situe en haut
de la fenêtre (elle est signalée par une flèche jaune dans la marge de gauche) ;
- la fonction mentionnée immédiatement en dessous est celle qui a appelé la fonction en cours
d'exécution ;
- celle de la troisième ligne est celle qui a appelé celle qui a appelé celle qui était en cours
d'exécution, et ainsi de suite.
Cette fenêtre nous permet donc de remonter l'enchaînement des appels, jusqu'à la fonction qui
nous intéresse : il suffit de repérer le premier nom (en partant d'en haut) qui correspond à celui
d'une
fonction
dont
nous
sommes
responsables.
Dans
le
présent
exemple,
CMonDialogue::OnButtonDelete() apparaît sur la quatrième ligne, et il suffit de doublecliquer sur ce nom pour que Visual C++ nous conduise à la ligne de code incriminée.
Le fait que nous sachions quelle ligne de code a effectué l'appel fautif de delete ne signifie
malheureusement pas que ce soit cette ligne de code qui nécessite une correction. C'est là qu'il
faut commencer à réfléchir, mais la fenêtre "Call Stack" nous offre, au moins, un point de
départ...
6 - Qu'avons nous appris ?
1) A raisonner de façon cohérente sur le contenu de pointeurs stockant des adresses obtenues
à l'aide de new.
2) A reconnaître certains des symptômes présentés par un programme utilisant l'allocation
dynamique d'une façon incohérente.
3) A interpréter les messages émis par Visual C++ en cas de fuite de mémoire.
4) A utiliser la fenêtre "Call Stack" du debugger.
Le tas (ou heap, en anglais) est la portion de mémoire utilisée pour l'allocation dynamique.
Si la fenêtre Call Stack n'est pas visible, faites-la apparaître : dans le menu View, choisissez "DebugWindows", et
dans ce sous menu, "Call Stack".
7
8
J-L Péris - 20/09/02
C++ - WinTD 07
Expériences avec new et delete
11/11
7 - Exercices
1 - Faites de m_lePointeur un "pointeur sur
double" (au lieu d'un "pointeur sur int") et
modifiez le programme en conséquences.
2 - Modifiez les fonctions OnButtonNew() et
OnButtonDelete() de façon à ce qu'elles
affichent des messages mentionnant combien
de fois elles ont déjà été appelées.
(Ne créez pas de nouvelle variable membre.)
3 - Modifiez la fonction OnButtonNew()de façon à
ce qu'elle place une valeur particulière (1789,
par exemple) dans la zone de mémoire dont elle
a obtenu l'usage grâce à new.
4 - Le TD 07 signale à plusieurs reprise que Visual
C++ adopte un comportement particulier
"pendant la phase de mise au point" d'un
programme. Indiquez que cette phase est
terminée
en
choisissant
"Set
Active
Configuration" dans le menu "Build". Le
dialogue ci-contre vous est alors proposé.
Exercice 2
Exercice 3
Sélectionnez la ligne "TD 07 - Win32 Release",
et cliquez sur OK.
Vous êtes désormais privé des sécurités
supplémentaires mises en place par Visual C++
pendant la phase de mise au point. Recompilez
le programme et observez les symptômes
présentés dans ce contexte lorsque vous
effectuez les séquences d'actions "anormales"
que nous avons pratiquées lors du TD 07.
Exercice 4
J-L Péris - 20/09/02

Documents pareils