First publication date : 2020/04/17
The skeleton of the two exercices can be cloned from here (not translated) :
git clone https://gitlab.com/kiux/CPP4.git
Red string ...
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 etud)
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