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