Laboratoire 4 : Classes génériques Pile<T> et File<T>
Lectures préalables
- Section 5 (Piles) des notes de cours (pages 48-51).
- L'implémentation pour le lab4 est celle avec le chaînage de cellules (section 5.3).
- Section 6 (Files) des notes de cours (pages 52-54).
- L'implémentation pour le lab4 / Tâche 6 est celle avec le chaînage de cellules (section 6.3).
Objectifs
- Analyser un programme existant pour identifier un bogue, et ce, sans l'exécuter.
- Faire la trace d'un programme sur papier (état de la mémoire).
- Compléter l'implémentation d'une pile.
- Implémenter une File.
Tâches
Téléchargement des fichiers du lab4
wget http://cria2.uqam.ca/INF3105/lab4/lab4.zip
unzip lab4.zip
Temps suggéré : 5 minutes.
Analyse sur papier
- Ouvrez et analysez les fichiers pile.h et lab4.cpp.
- Sans compiler ni exécuter le programme lab4.cpp, faites la trace sur papier de son exécution.
- Cela consiste à dessiner l'état de la mémoire (la pile d'exécution et le tas (heap)).
Inspirez-vous des schémas dans les notes de cours et ceux produits au tableau en classe.
- Le programme lab4.cpp + pile.h contient un bogue.
- Identifiez le(s) problème(s) avec précision :
- de quel type (compilation, bon fonctionnement, gestion mémoire, etc.) s'agit-il ?
- à quelle(s) ligne(s) cela se produit-il ?
- quelle est la cause ?
- Indices :
- Comment le paramètre Pile pile de fonction1() est-il construit?
- Qu'arrive-t-il si le constructeur de Pile<T>::Pile(const Pile&) n'est pas déclaré/défini?
- Comment ce constructeur est-il synthétisé par le compilateur?
Temps suggéré : 15 minutes.
Expérimentation
- Compilez le lab4 avec la commande « make lab4 ».
- Lancez lab4 avec la commande « ./lab4 ».
- Vous pouvez aussi essayer avec la commande «valgrind ./lab4 ».
- Si le programme ne s'arrête pas, faites [Contrôle] + C.
- Pour suivre la trace d'exécution du programme, vous pouvez :
- ajouter des «std::cerr << "xxxxx" << std::endl;» à quelques endroits;
- utiliser un débogueur :
- GDB en ligne de commande (la section 2.19 des notes de cours explique les principales commandes);
- Un débogueur intégré dans votre IDE (e.g., Code::Blocks).
- Est-ce que les observations confirment vos réponses à la tâche 2?
Temps suggéré : 15 minutes.
Débogueur GDB
- Assurez-vous que lab4 ait été compilé avec l'option « -ggdb » de g++.
- Lancez le débogueur avec « gdb lab4 ».
- GDB est un débogueur en mode interactif (commandes).
- Lancez l'exécution avec « run ».
- Si l'exécution ne se termine pas, faites [Contrôle] + C une
seule fois.
- Vérifiez l'état de la pile d'exécution (call stack) avec la
commande «bt» ou «backtrace».
- La commande «help» permet de voir la liste des commandes.
- La commande «quit» permet de quitter.
Débogueurs dans les environnements de développement intégré
- Les débogueurs intégrés sont intuitifs.
- Si vous avez besoin d'assistance, demandez l'aide du démonstrateur.
Corrections
- Réfléchissez aux corrections nécessaires.
- Implémentez les corrections.
- Vérifiez.
Temps suggéré : 20 minutes.
Tests unitaires pour Pile<T>
- make test_pile
- ./test_pile
- Faites les modifications pour que tous les tests passent.
Temps suggéré : 25 minutes.
Tests unitaire pour File<T>
- make test_file
- ./test_file
- Faites les modifications pour que tous les tests passent.
Temps suggéré : 30 minutes.
Exercices complémentaires
Vérification de la libération de la mémoire
Les tests unitaires proposés ne valident pas la libération correcte de la mémoire.
Comment pourriez-vous vérifier si la mémoire est correctement libérée?
Il y a plusieurs stratégies possibles.
La méthode la plus simple est une méthode empirique consistant à faire un grand nombre d'opérations et vérifier la consommation de mémoire.
Si jamais une fuite de mémoire (memory leak) était présente, la quantité de mémoire utilisée par le processus augmenterait continuellement.
Voici un programme permettant une telle vérification.
int main() {
int n = 0;
while(true) {
cout << n++ << " ";
Pile<int> pile1;
for(int i = 0; i < 100000; i++)
pile1.empiler(i);
Pile<int> pile2 = pile1;
pile1.vider();
Pile<int> pile3;
pile3.empiler(10);
pile3 = pile2;
pile3 = pile1;
}
}
Procédure :
- Dans un premier terminal, compilez le programme ci-haut.
- Optionnel : tapez dans le premier terminal la commande « ulimit -v 32768 » afin de fixer la limite de mémoire à 32 Mo.
- Lancez le programme ci-haut dans le premier terminal.
- Dans un deuxième terminal, lancez la commande « top » ou « htop » afin de voir les processus en cours d'exécution.
- Dans top, appuyez une fois sur 'u' et tapez votre nom d'usager Unix.
Cela vous permettra de voir uniquement vos processus.
- Repérez votre programme en cours d'exécution.
- Vérifiez si la mémoire consommée (colonne SIZE) est constante.
Si la taille augmente constamment, il y a peut-être un problème.
- Pour vous assurez que cette procédure permet de détecter des fuites de mémoires, essayez d'enlever les appels à «delete» dans pile.h, et refaites les tests.
La méthode ci-haut est empirique. Si la fuite de mémoire est petite, cela prendra beaucoup de temps avant de la détecter.
De plus, cela ne teste pas tous les cas possibles.
À noter qu'il existe des outils permettant de détecter les fuites de mémoire.
L'un d'eux est Valgrind.
Intégration avec Tableau<>
Il est possible de combiner les structures de données vues jusqu'à présent.
Par exemple, on peut faire une tableau de piles!
Essayez le programme suivant.
#include "tableau.h"
#include "pile.h"
int main() {
Tableau<Pile<int>> tab1;
for(int i = 0; i < 100; i++) {
Pile pile<int>;
pile.empiler(-1);
tab1.ajouter(pile);
for(int j = 0; j < i; j++)
tab1[i].empiler(j);
}
Tableau<Pile<int>> tab2;
tab2 = tab1;
for(int i = 0; i < 100; i++)
tab1[i] = Pile<int>();
}
Opérateur = efficace
- Voici l'opérateur Pile<T>::operator=() présenté dans les notes de cours.
Pile& Pile::operator = (const Pile& autre) {
if(this == &autre) return *this; // cas spécial lorsqu'on affecte un objet à lui-même
vider(); // ou: while(sommet != nullptr){ Cellule* t = sommet->suivante; delete sommet; sommet = t; }
if(autre.sommet != nullptr) {
sommet = new Cellule(autre.sommet->element);
Cellule* cp = autre.sommet;
Cellule* c = sommet;
while(cp->suivante != nullptr) {
c->suivante = new Cellule(cp->suivante->element);
c = c->suivante;
cp = cp->suivante;
}
}
return p;
}
- Ce dernier commence par vider la pile. Ensuite, il recrée de nouvelles cellules en parcourant celles de la pile autre.
- Il est possible de faire mieux en réutilisant les cellules existantes.
- Il y a 3 cas :
- les 2 piles ont le même nombre d'éléments;
- la pile implicite (*this) contient moins d'éléments que la pile reçue en paramètre;
- la pile implicite (*this) contient plus d'éléments que la pile reçue en paramètre.