Date de première publication : 2013/09/24
Causettes
Les exercices suivants proposent d'analyser quelques situations afin d'éviter de les reproduire
On réutilise la classe Bavarde développée au TP précédent
Tableaux verbeux
Ajouter une méthode afficher() qui affiche sur la sortie standard "Affichage de %" et exécuter le code suivant.
int main(int, char **) {
const int TAILLE = 20;
Bavarde tab1[TAILLE];
Bavarde * tab2 = new Bavarde[TAILLE];
// Combien d'instances sont créées ?
for (int i =0; i < TAILLE; ++i) {
tab1[i].afficher();
tab2[i].afficher();
}
// Combien d'instances sont détruites ?
return 0;
}
Évidemment, la mémoire allouée à tab2 n'est pas libérée...Ce qu'il faut faire avec la version adaptée de delete.
Il faut également noter que des instances ont été créées lors de la création des tableaux.
Objet complexe : un couple
Écrire une classe Couple qui possède deux attributs de type Bavarde.
- Instancier un
Coupleet vérifier que "trois" objets sont bien créés et bien détruits. - Utiliser une liste d'initialisation pour donner une valeur distincte aux deux objets de type
Bavarde - Vérifier que l'ordre de création est bien l'inverse de l'ordre de destruction.
- Si les attributs ont été initialisés dans le bon l'ordre, permuter les initialisations pour découvrir le message d'avertissement idoine.
L'option -Wreorder permet d'être prévenu de cette situation, elle est incluse dans -Wall
Objet complexe : une famille nombreuse
- Écrire une classe
Famillequi définit un pointeur pour manipuler un tableau d'éléments de typeBavarde - Doter cette classe d'un constructeur qui alloue un tableau dont la taille de la famille est fournie en paramètre. La valeur 0 est admissible
- Tester un programme qui instancie un ou plusieurs
Familleavec valgrind et remarquer ce qu'il se passe. - Ajouter le destructeur qui va bien
malloc/free vs new/delete
Instancier un objet de classe Bavarde avec un malloc(). Afficher le champ "valeur" de l'objet. Que se passe-t-il ? (à comparer avec l'utilisation de new)
Exécutez valgrind !!!
Pour inclure un fichier d'entête C (malloc() et free() sont définies dans stdlib.h), c'est facile, il faut préfixer par -c- et omettre l'extension :
#include <cstdlib>
Héritage simple public
Illustration
- Définir une classe
Meredont le constructeur et le destructeur affichent quelque chose à l'écran (comme la classeBavarde). - Définir une classe
Fillequi hérite (publiquement) deMere. Ne rien mettre pour l'instant dans cette classe. - Instancier une classe
Fille. Que se passe-t-il ?
Un objet de classe Fille est avant tout un objet de la classe Mere, c'est pourquoi construire un nouvel objet Fille fait appel en premier lieu au constructeur de la classe Mere.
La destruction se fait en sens inverse.
- Pour mieux observer le phénomène, implémenter un constructeur et un destructeur "bavards" pour la classe
Fille. - Si le constructeur de
Fillene mentionne pas celui deMere, que se passe-t-il ?
Un objet de classe Fille est un objet de classe Mere donc le constructeur de la classe mère par défaut est appelé, même si on ne le précise pas. Je préconise de le spécifier tout de même, cela "documente" le code.
- Modifier le constructeur par défaut de
Fillepour qu'il appelle explicitement celui deMere - Ajouter un attribut entier dans la classe
Mere - Vérifier la visibilité de celui-ci dans une méthode de la classe
Filles'il est privé, protégé ou public. Par la suite, on considérera cet attribut non public. - Utiliser cet attribut pour compter le nombre d'instances de
Merequi sont créées (ce nombre sera affiché à l'instanciation) - Vérifier que ce nombre est bien incrémenté à l'instanciation d'une classe
Fille.
Il faut un attribut de classe dans la classe Mere pour faire cela
- Ajouter une méthode
getCompteur()(getter) sur cet attribut entier - Appeler cette méthode à partir d'un objet de la classe
Mere, puis d'un objet de la classeFille.
On peut bien dire que la méthode a été héritée car elle est disponible sans avoir eu à la réécrire dans la classe Fille.
- Vous pouvez même appeler la méthode
getCompteur()sans objet dans la fonctionmain()par exemple. - Ajouter un attribut
nomde type chaine de caractèresstd::stringà la classeMere - Ajouter un constructeur qui permet d'initialiser cet attribut avec un paramètre donné
- Ajouter une méthode
getName()sur cet attribut - Instancier un objet de la classe
Mereavec ce constructeur - Appeler la méthode
getName()à partir d'un objet de la classeMere, puis d'un objet de la classeFille. - Essayer d'instancier un objet de la classe
Filledont lenomest donné à l'initialisation, que se passe-t-il ?
Le constructeur avec paramètre de Mere n'a pas été hérité. Il faut en écrire un pour Fille.
En C++11, il est possible de récupérer les constructeurs de la classe mère.
- Écrire un constructeur de
Fillequi prend en paramètre une chaîne de caractères et qui appelle explicitement celui de la classeMere. - Que se passe-t-il si le constructeur de
Mereavec la chaîne de caractères n'est pas spécifié ? - Ajouter maintenant une méthode
afficher()dans la classeMerequi affiche sur la sortie standard que l'objet est de classeMere - Faire de même pour la classe
Fille - Vérifier que l'exécution est correcte pour une instance de
Mereet une instance deFille - Vérifier maintenant que l'exécution est correcte pour le code ci-dessous. Si ce n'est pas le cas, corriger votre code pour que cela marche
Mere *pm = new Mere("mere_dyn");
Fille *pf = new Fille("fille_dyn");
Mere *pp = new Fille("fille vue comme mere");
pm->afficher(); // affiche Mere
pf->afficher(); // affiche Fille
pp->afficher(); // affiche Fille
Une petite question ...
Que fait le programme suivant ?
class Mere {
protected:
std::string nom;
public:
Mere(string s="pas fourni"):nom(s) {
}
void methode1() {
std::cout << "Methode1(): " << nom << std::endl;
}
};
class Fille : public Mere {
private:
std::string nom;
public:
Fille():Mere("noname") {
}
void methode2() {
std::cout << "Methode2(): " << nom << std::endl;
}
};
int main(int, char**) {
Fille f;
f.methode1();
f.methode2();
}
Vous pouvez copier-coller ou retaper le code mais vous pouvez aussi le récupérer grâce à git (question.cpp)
Si vous avez configuré l'environnement git, tapez les commandes suivantes :
git clone https://gitlab.com/kiux/CPP3.git
- Amusez-vous à déclarer la
methode1()virtuelle dansMere. Que se passe-t-il ? - Copier
methode1()dansFille. Que se passe-t-il ?
L'attribut nom de Mere est masqué dans la classe Fille. Pour le retrouver, il faut
utiliser Mere::nom
Messages
Cet exercice n'est pas difficile au niveau de la modélisation mais il est nécessaire de bien séparer la déclaration de l'implémentation. Vous ne devez pas y passer plus de 10 minutes
Écrire deux classes A et B. La classe A possède un entier i, et la classe B un entier j.
Ces deux classes ont chacune une méthode exec() et une méthode send() qui leur permet d’envoyer un message à un objet de l’autre classe.
- La méthode
send()de la classeAaccepte un pointeur sur un objet de classeBet réciproquement. - La méthode
exec()de chaque classe accepte un entier en paramètre et ajoute la valeur de cet entier aux attributsioujselon la classe de l’objet concerné (AouB).
L’exécution du corps d’une méthode send() lance un exec() sur l’objet distant avec une constante de votre choix. Ainsi unA.send(&unB) active la méthode send() de la classe A qui lance la méthode exec() de la classe B.
Pour que cet exercice soit formateur, il faut :
- utiliser un
makefilepour gérer les fichiersA.hpp,A.cpp,B.hpp,B.cppetmain.cpp - les déclarations des classes doivent être correctes dans les fichiers d'entête avec des gardiens
- Utiliser les déclarations anticipées (encore appelées forward) des classes.
La classe B a besoin de la classe A et la classe A a besoin de la classe B. Les déclarations de classe se mordent la queue. Pour que cela marche, il faut utiliser les déclarations anticipées
mais il faut surtout ne manipuler que des références et des pointeurs sur des objets de l'autre classe sous peine de ne pas arriver à instancier des objets qui ne sont pas complètement définis.
Exercice de modélisation
Pour faire cet exercice, il faut tout d'abord lire la fiche sur les flux et la fiche sur les tests. Le squelette du programme à écrire se trouve également sur le git :
git clone https://gitlab.com/kiux/CPP3.git
Nous voulons rendre service à un personne qui fait des statistiques sur des données provenant de différentes sources (des producteurs).
- Déclarer une classe
Producteuravec un attributtravail. L'attribut donne le nombre de fois où la méthodeproduire()a été appelée.
TEST_CASE("Producteur_Initialisation") {
Producteur p;
REQUIRE( p.getTravail() == 0);
}TEST(Producteur, Initialisation) {
Producteur p;
ASSERT_EQ(0, p.getTravail());
}- Ajouter une méthode
produire()qui prend en paramètre un entier (un nombre de nombres à générer) et une chaîne de caractères correspondant au fichier à écrire.
TEST_CASE("Producteur_travail2") {
Producteur p;
p.produire(10, "test01.txt");
p.produire(10, "test01.txt");
p.produire(10, "test01.txt");
REQUIRE( p.getTravail() == 3);
}TEST(Producteur, Travail2) {
Producteur p;
p.produire(10, "test01.txt");
p.produire(10, "test01.txt");
p.produire(10, "test01.txt");
ASSERT_EQ(3, p.getTravail());
}- Écrire l'implémentation de la méthode
produire()où la méthode génère les premiers entiers (le fichier résulat mentionne d'abord le nombre d'éléments puis les éléments un à un)
TEST_CASE("Producteur_Travail3") {
const int DEMANDE = 10;
const std::string NOM_FICHIER("test01.txt");
int lecture, i;
Producteur p;
p.produire(DEMANDE, NOM_FICHIER.c_str());
std::ifstream fichier(NOM_FICHIER.c_str());
REQUIRE(fichier.is_open());
if (!fichier.eof()) {
fichier >> lecture;
REQUIRE(DEMANDE == lecture);
for (i = 0; i < DEMANDE; ++i) {
fichier >> lecture;
REQUIRE( lecture == (i+1) );
}
REQUIRE(i == DEMANDE);
// CHECK(fichier.eof());
fichier.close();
REQUIRE(p.getTravail() == 1);
}
}TEST(Producteur, Travail3) {
const int DEMANDE = 10;
const std::string NOM_FICHIER("test01.txt");
int lecture, i;
Producteur p;
p.produire(DEMANDE, NOM_FICHIER.c_str());
std::ifstream fichier(NOM_FICHIER.c_str());
ASSERT_TRUE(fichier.is_open());
if (!fichier.eof()) {
fichier >> lecture;
EXPECT_EQ(DEMANDE, lecture);
for (i = 0; i < DEMANDE; ++i) {
fichier >> lecture;
EXPECT_EQ(i+1, lecture);
}
EXPECT_EQ(i, DEMANDE);
// EXPECT_TRUE(fichier.eof());
fichier.close();
ASSERT_EQ(p.getTravail(), 1);
}
}- Créer maintenant une classe
Statisticienqui disposera d'une méthodeacquerir()avec un paramètre, le nom du fichier à lire. La classe sera dotée d'un attributcalculqui sera vrai si des calculs sont disponibles et faux sinon.
TEST_CASE("Statisticien_Initialisation") {
Statisticien p;
REQUIRE_FALSE(p.aCalcule());
}TEST(Statisticien, Initialisation) {
Statisticien p;
ASSERT_FALSE(p.aCalcule());
}- Implémenter la méthode
acquerir(), la méthode lit les nombres du fichier et calcule différentes choses : on s'intéressera à la somme et la moyenne. On pourra par exemple vérifier que la lecture est cohérente avec l'écriture. La somme des n premiers entiers est n*(n+1)/2. Je vous laisse écrire le code de test ! - Écrire maintenant une nouvelle classe
ProducteurAleatoireoù les nombres générés le sont aléatoirement
Si vous avez vu en cours la notion de classe abstraite :
- Modifier la hiérachie de classes pour que la classe
Producteursoit maintenant abstraite et que la classeProducteurPremiersEntiersfasse ce que la classeProducteurfaisait avant. - Vérifier que l'instanciation de la classe
Producteurn'est pas possible. On ne peut pas vérifier cela avec les tests unitaires
Fil rouge ...gesture
Nous allons continuer l'application fil rouge.
- Créer une classe
Listequi a pour attributs deux tableaux : un deCercleet un deRectanglede capacité fixe (une constante vraie définie avec unconst, PAS une constante symbolique). Ces attributs sont publics, ce n'est pas très beau mais relativement nécessaire. On connaitra également le nombre d'éléments vraiment placés dans chaque tableau (leur "taille") à savoirnb_cetnb_r.
| Liste |
| + cercles : tableau + nb_c : entier + rectangles : tableau + nb_r : entier |
| + Liste() + getCompteur() : entier + toString() : chaine + ajouter(Cercle) + ajouter(Rectangle) |
La méthode getCompteur() renvoie le nombre d'éléments dand la liste.
Si vous voulez respecter l'encapsulation, utilisez des tableaux de pointeurs.
- Ajouter aux classes
RectangleetCercleun nouvel attributordre. Cet attribut sera initialisé par l'objetListeà chaque fois qu'un objet est ajouté. On suppose évidemment qu'un objet ne peut appartenir qu'à une seule liste à la fois. - Proposer une méthode
toString()qui renvoie dans une chaîne de caractères la liste des rectangles et des cercles contenus dans la liste. Vous pourrez afficher les listes une par une mais à la fin, il faudra afficher les éléments dans l'ordre où ils sont été ajoutés.
Illustrons la manière dont une liste gère l'ordre des éléments :
Liste l;
// nb_r == nb_c = 0
Rectangle r1(10, 10, 20, 20);
l.ajouter(r1);
// nb_r ==1, nb_c == 0, "r1.ordre" == 1
Cercle c1(15, 15, 5);
l.ajouter(c1);
// nb_r ==1, nb_c == 1, "c1.ordre" == 2
Rectangle r2(20, 20, 30, 30);
l.ajouter(r2);
// nb_r ==2, nb_c == 1, "r2.ordre" == 2
Cette manière de stocker les objets n'est ni pratique ni efficace, le C++ nous permet de faire bien mieux avec le modèle objet, ce que l'on fera plus tard !
- Créer une classe
Pointqui a pour propriété une abscissexet une ordonnéey - Créer une classe
Formequi a pour propriété un point, une largeurwet une hauteurh - Ajouter un attribut de classe
nbFormesqui est incrémenté à chaque fois qu'un objetFormeest construit. - Vérifier bien entendu que l'instanciation d'une forme se passe bien et est cohérente avec l'attribut de classe.
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10



