Le TP de la semaine dernière était très dense. Terminez le TP précédent avant de faire celui-ci
Date de première publication : 2013/10/03
Petites vérifications
Avez-vous vérifié les choses suivantes ?
- que le passage d'un objet se fait par défaut par copie. Pour cela, il suffit de réutiliser
la classe
Bavarde
, de passer une variable à une fonction quelconque et de compter le nombre de destructions
void test1(Bavarde b) {
std::cout << "appel de fonction avec parametre objet et copie";
}
- que le retour de méthode/fonction crée aussi une copie.
Bavarde test2a() {
std::cout << "appel de fonction avec retour";
return Bavarde(); // creation d'un objet local
} // plus de copie - voir ZZ3
Bavarde test2b() {
Bavarde b; // creation d'un objet local
std::cout << "appel de fonction avec retour";
return b;
} // plus de copie - ZZ3
- Ajoutez maintenant un constructeur de copie à la classe
Bavarde
qui incrémentera le nombre d'instances et vérifer qu'il est bien appelé dans les situations précédentes - que l'utilisation d'une référence ne crée pas de copie,
void test3(Bavarde& b) {
std::cout << "appel de fonction avec référence ";
}
- et l'utilisation d'un pointeur non plus
void test4(Bavarde *b) {
std::cout << "appel de fonction avec un pointeur sur un objet";
}
Troncature de type
Vérifier ce qu'est la troncature de type et comment on l'évite ...
void afficher1(Forme f) {
f.afficher();
}
void afficher2(Forme &f) {
f.afficher();
}
void afficher3(Forme * f) {
f->afficher();
}
int main(int, char**) {
Cercle cercle;
afficher1(cercle);
afficher2(cercle);
afficher3(&cercle);
return 0;
}
Fil rouge ...
La base des deux exercices est clonable ici :
git clone https://gitlab.com/kiux/CPP4.git
Vous avez écrit les classes Rectangle
et Cercle
. L'idée est de mettre désormais en place l'héritage pour qu'un rectangle (respectivement un cercle) soit une forme. Nous allons ajouter également la composition/agrégation avec la classe Point
Classe Point
Une instance particulière de la classe Point
, ORIGINE
sera définie et utilisée. Cette origine servira de référence quand la position de la forme ne sera pas précisée à la construction (une constante globale fait l'affaire).
TEST_CASE("Instanciation", "[Point]") {
Point p1;
REQUIRE(p1.getX() == 0);
REQUIRE(p1.getY() == 0);
p1.setX(11);
p1.setY(21);
REQUIRE(p1.getX() == 11);
REQUIRE(p1.getY() == 21);
Point p2(12, 22);
REQUIRE(p2.getX() == 12);
REQUIRE(p2.getY() == 0); // :-)
}
TEST_CASE("Origine", "[Point]") {
REQUIRE(ORIGINE.getX() == 0);
REQUIRE(ORIGINE.getY() == 0);
}
TEST(Point, Instanciation) {
Point p1;
ASSERT_EQ(p1.getX(), 0);
ASSERT_EQ(p1.getY(), 0);
p1.setX(11);
p1.setY(21);
ASSERT_EQ(p1.getX(), 11);
ASSERT_EQ(p1.getY(), 21);
Point p2(12, 22);
ASSERT_EQ(p2.getX(), 12);
ASSERT_EQ(p2.getY(), 0); // :-)
}
TEST(Point, Origine) {
ASSERT_EQ(ORIGINE.getX(), 0);
ASSERT_EQ(ORIGINE.getY(), 0);
}
Si vous voulez définir ORIGINE
comme un membre public constant de classe Point
, c'est tout-à-fait possible, modifiez les tests en conséquence. De l'intérieur de la classe, ORIGINE
est directement accessible. De l'extérieur, il faudra préfixer par le nom de la classe qui définit ce point particulier Point::ORIGINE
Classe Forme
Passons maintenant à la classe Forme
. Ajoutez un attribut couleur
de type COULEURS
. COULEURS
peut être une énumération classique (à la C) mais aussi une énumération typée (enum class), concept apparu avec la norme 2011 du langage.
enum class COULEURS {
NOIR, BLANC
};
COULEURS couleur = COULEURS::BLANC;
Les enum class
ont de nombreux avantages par rapport aux énumérations classiques dont notamment la portée limitée des valeurs (on est obligé de préfixer la valeur par le nom de l'énumération) et la non conversion implicite en entier
TEST_CASE("Instanciation1", "[Forme]") {
Forme f1;
REQUIRE(f1.getPoint().getX() == 0);
REQUIRE(f1.getPoint().getY() == 0);
REQUIRE(f1.getCouleur() == COULEURS::BLEU);
}
TEST_CASE("Instanciation2", "[Forme]") {
Forme f2;
f2.setX(15);
f2.setY(25);
f2.setCouleur(COULEURS::VERT);
REQUIRE (f2.getPoint().getX() == 15);
REQUIRE (f2.getPoint().getY() == 25);
REQUIRE (f2.getCouleur() == COULEURS::VERT);
REQUIRE_FALSE (f2.getCouleur() == COULEURS::BLEU);
REQUIRE_FALSE (f2.getCouleur() == COULEURS::ROUGE);
REQUIRE_FALSE (f2.getCouleur() == COULEURS::JAUNE);
}
TEST_CASE("Instanciation3", "[Forme]") {
// SI LE TEST NE MARCHE PAS, VOUS AVEZ UNE ERREUR DANS VOTRE CODE
Forme f2(Point(10,20), COULEURS::ROUGE);
REQUIRE (f2.getPoint().getX() == 10);
REQUIRE (f2.getPoint().getY() == 20);
REQUIRE (f2.getCouleur() == COULEURS::ROUGE);
REQUIRE_FALSE (f2.getCouleur() == COULEURS::BLEU);
f2.getPoint().setX(15);
f2.getPoint().setY(25);
f2.setCouleur(COULEURS::JAUNE);
REQUIRE (f2.getPoint().getX() == 15);
REQUIRE (f2.getPoint().getY() == 25);
REQUIRE (f2.getCouleur() == COULEURS::JAUNE);
REQUIRE_FALSE (f2.getCouleur() == COULEURS::BLEU);
REQUIRE_FALSE (f2.getCouleur() == COULEURS::ROUGE);
}
TEST(Forme, Instanciation1) {
Forme f1;
ASSERT_EQ(f1.getPoint().getX(), 0);
ASSERT_EQ(f1.getPoint().getY(), 0);
ASSERT_EQ(f1.getCouleur(), BLEU);
}
TEST(Forme, Instanciation2) {
Forme f2;
f2.setX(15); // pas une erreur, faut essayer !!!
f2.setY(25);
f2.setCouleur(VERT);
ASSERT_EQ (f2.getPoint().getX(), 15);
ASSERT_EQ (f2.getPoint().getY(), 25);
ASSERT_EQ (f2.getCouleur(), VERT);
ASSERT_NE (f2.getCouleur(), BLEU);
ASSERT_NE (f2.getCouleur(), ROUGE);
ASSERT_NE (f2.getCouleur(), JAUNE);
}
TEST(Forme, Instanciation3) {
// IL N'Y A PAS D'ERREUR DANS LE TEST, CELA DOIT MARCHER
Forme f2(Point(10,20), ROUGE);
ASSERT_EQ (f2.getPoint().getX(), 10);
ASSERT_EQ (f2.getPoint().getY(), 20);
ASSERT_EQ (f2.getCouleur(), ROUGE);
ASSERT_NE (f2.getCouleur(), BLEU);
f2.getPoint().setX(15);
f2.getPoint().setY(25);
f2.setCouleur(JAUNE);
ASSERT_EQ (f2.getPoint().getX(), 15);
ASSERT_EQ (f2.getPoint().getY(), 25);
ASSERT_EQ (f2.getCouleur(), JAUNE);
ASSERT_NE (f2.getCouleur(), BLEU);
ASSERT_NE (f2.getCouleur(), ROUGE);
}
Vous allez doter également la classe d'un attribut id
qui est un indentifiant unique de la forme (on s'appuiera sur l'attribut nbFormes
.
Nous ne devrions pas avoir besoin de publier cet attribut de classe (dans un usage normal de la classe) mais pour les tests, il va tout de même falloir le faire. La méthode associée s'appelle prochainId()
et permet un accès en lecture seulement de cet attribut.
TEST_CASE("Compteur", "[Forme]") {
// Pour être correct, ce test doit etre le premier sur Forme
REQUIRE(0 == Forme::prochainId());
Forme f1;
REQUIRE(0 == f1.getId());
REQUIRE(1 == Forme::prochainId());
// Verification que la valeur n'est pas decrementee accidentellement.
Forme *p = new Forme;
REQUIRE(1 == p->getId());
delete p;
REQUIRE(2 == Forme::prochainId());
}
TEST(Forme, Compteur) {
// Pour être correct, ce test doit etre le premier sur Forme
ASSERT_EQ(0, Forme::prochainId());
Forme f1;
ASSERT_EQ(0, f1.getId());
ASSERT_EQ(1, Forme::prochainId());
// Verification que la valeur n'est pas decrementee accidentellement.
Forme *p = new Forme;
ASSERT_EQ(1, p->getId());
delete p;
ASSERT_EQ(2, Forme::prochainId());
}
Classes Rectangle
et Cercle
Écrivez la relation d'héritage entre les classes Rectangle
et Cercle
et la classe Forme
. Nettoyez les méthodes et attributs non nécessaires ! La création d'un rectangle ou d'un cercle doit incrémenter natuellement le compteur de formes.
TEST_CASE("Cercle", "[Cercle]") {
int compteur = Forme::prochainId();
Cercle c1;
Cercle c2(...);
REQUIRE(c1.toString() == ".....");
REQUIRE(c2.toString() == ".....");
c2.setRayon(...);
REQUIRE(c2.getRayon() == "..." );
REQUIRE(c2.toString() == ".....");
REQUIRE(c2.getLargeur() == ".....");
REQUIRE(c2.getHauteur() == ".....");
REQUIRE(Forme::prochainId() == (compteur+2) );
}
TEST(Forme, Cercle) {
int compteur = Forme::prochainId();
Cercle c1;
Cercle c2(...);
EXPECT_EQ(c1.toString(), ".....");
EXPECT_EQ(c2.toString(), ".....");
c2.setRayon(...);
EXPECT_EQ(c2.getRayon(), );
EXPECT_EQ(c2.toString(), ".....");
EXPECT_EQ(c2.getLargeur(), ".....");
EXPECT_EQ(c2.getHauteur(), ".....");
EXPECT_EQ(Forme::prochainId(), compteur+2);
}
On pourra ensuite vérifier le polymorphisme fort (les tests sont à adapter en fonction de ce que renvoient les méthodes toString()
)
TEST_CASE("Polymorphisme", "[Forme]") {
Forme * f1 = new Cercle;
Forme * f2 = new Rectangle;
REQUIRE(f1->toString() == ".....");
REQUIRE(f2->toString() == ".....");
delete f1;
delete f2;
}
TEST(Forme, Polymorphisme) {
Forme * f1 = new Cercle;
Forme * f2 = new Rectangle;
EXPECT_EQ(f1->toString(), ".....");
EXPECT_EQ(f2->toString(), ".....");
delete f1;
delete f2;
}
La classe Forme
devra être abstraite et vous devrez choisir quelles méthodes sont virtuelles (pures ou non). Dès que la classe aura été modifiée pour être abstraite, les premiers tests avec instanciation ne seront plus utilisables (le compilateur n'acceptera plus leur compilation)
Classe Liste
Le code de la classe Liste
est maintenant bien plus facile à écrire !
Vous allez transformer la classe Liste
en une classe Groupe
. On aurait pu faire de l'héritage multiple mais on en l'a pas encore vu en cours, n'est-ce pas ?
Un groupe est une forme. Dans cette classe, le point, la largeur et la hauteur sont calculés comme étant ceux de la boîte englobante et mis à jour lors des opérations d'ajout et de suppression des formes au groupe. Le polymorphisme et l'upcasting permettent de vérifier que la relation d'héritage est correcte.
Le groupe peut servir à stocker une scène à afficher.
Aller plus loin...
Vous pouvez coder une interface texte qui répondra aux messages suivants
$ creer cercle 10 10 30 30
=> 1
$ creer cercle 40 40 20
=> 2
$ creer rectangle 30 30 15 15
=> 3
$ afficher
CERCLE 1 10 10 30 30
CERCLE 2 20 20 40 40
RECTANGLE 3 30 30 15 15
$ creer groupe
=> 4
$ ajouter 4 1
$ ajouter 4 2
$ afficher
GROUPE 4 10 10 40 40
CERCLE 1 10 10 30 30
CERCLE 2 20 20 40 40
RECTANGLE 3 30 30 15 15
$ contient 3 40 40
true
Une pincée de profilage
Voici un petit programme à analyser avec un profiler. On pourra par exemple compter le nombre de fois que certaines méthodes sont appelées
#include<iostream>
#include<cmath>
#include<cstdlib>
class Element {
double x, y;
bool ajour;
double distance;
void calculerDistance();
public:
Element();
Element(double, double);
void setX(double);
void setY(double);
double getDistance();
double getDistance2();
};
Element::Element():x(.0), y(.0), ajour(true), distance(.0){
}
Element::Element(double px, double py):x(px), y(py), ajour(false), distance(.0) {
}
void Element::calculerDistance() {
distance = sqrt(x*x+y*y);
ajour = true;
}
void Element::setX(double px) {
x = px;
ajour = false;
}
void Element::setY(double py) {
y = py;
ajour = false;
}
double Element::getDistance() {
if (!ajour)
calculerDistance();
return distance;
}
double Element::getDistance2() {
calculerDistance();
return distance;
}
int main(int, char**) {
Element e(10.0, 100.0);
for(int i=0;i<100000; ++i) {
if (!(random() % 7) ) {
e.setX((double)rand());
e.setY((double)rand());
}
std::cout << e.getDistance() << " ";
std::cout << e.getDistance2() << std::endl;
}
return 0;
}
Rappel : gprof
Pour profiler avec gprof, il suffit :
- de compiler le programme avec l'option -pg.
- de lancer l'exécutable : cela génère un fichier gprof.out
- d'analyser le fichier en le donnant à gprof.
Note : un programme compilé avec l'option -pg est beaucoup plus long à s'exécuter.
Valgrind
Nous pouvons également utiliser ce couteau suisse de la programmation : valgrind. Il suffit pour cela de choisir un autre outil que celui par défaut (memcheck).
- Compiler avec l'option -g
- Exécuter
valgrind --tool=callgrind --dump-instr=yes executable
- Analyser les résultats avec
kcachegrind (installé sur les machines LINUX)
Note : valgrind utilise une simulation de processeur, l'exécution peut donc être 50 fois plus longue que le programme original, l'avantage est que le temps de calcul utilisateur ne dépend pas de la charge machine
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10