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
Couple
et 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
Famille
qui 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
Famille
avec 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
Mere
dont le constructeur et le destructeur affichent quelque chose à l'écran (comme la classeBavarde
). - Définir une classe
Fille
qui 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
Fille
ne 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
Fille
pour 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
Fille
s'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
Mere
qui 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
nom
de 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
Mere
avec 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
Fille
dont lenom
est 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
Fille
qui 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
Mere
avec la chaîne de caractères n'est pas spécifié ? - Ajouter maintenant une méthode
afficher()
dans la classeMere
qui 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
Mere
et 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 classeA
accepte un pointeur sur un objet de classeB
et réciproquement. - La méthode
exec()
de chaque classe accepte un entier en paramètre et ajoute la valeur de cet entier aux attributsi
ouj
selon la classe de l’objet concerné (A
ouB
).
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
makefile
pour gérer les fichiersA.hpp
,A.cpp
,B.hpp
,B.cpp
etmain.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
Producteur
avec 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
Statisticien
qui disposera d'une méthodeacquerir()
avec un paramètre, le nom du fichier à lire. La classe sera dotée d'un attributcalcul
qui 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
ProducteurAleatoire
où 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
Producteur
soit maintenant abstraite et que la classeProducteurPremiersEntiers
fasse ce que la classeProducteur
faisait avant. - Vérifier que l'instanciation de la classe
Producteur
n'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
Liste
qui a pour attributs deux tableaux : un deCercle
et un deRectangle
de capacité fixe (une constante vraieconst
, PAS une constante symbolique). Ces attributs sont publics, ce n'est pas très beau mais relativement nécessaire. Si vous voulez respecter l'encapsulation, utilisez des tableaux de pointeurs. On connaitra également le nombre d'éléments vraiment placés dans chaque tableau (leur "taille").
Liste |
+ cercles : tableau + nb_c : entier + rectangles : tableau + nb_r : entier + compteur : entier |
+ Liste() + getCompteur() : entier + toString() : chaine |
- Ajouter aux classes
Rectangle
etCercle
un 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.
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
Point
qui a pour propriété une abscissex
et une ordonnéey
- Créer une classe
Forme
qui a pour propriété un point, une largeurw
et une hauteurh
- Ajouter un attribut de classe
nbFormes
qui est incrémenté à chaque fois qu'un objetForme
est 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