• Aucun résultat trouvé

Travaux Pratiques

N/A
N/A
Protected

Academic year: 2022

Partager "Travaux Pratiques"

Copied!
14
0
0

Texte intégral

(1)

http://robert.cireddu.free.fr/SNIR/

http://robert.cireddu.free.fr/SNIR/

Extrait du référentiel : BTS Systèmes Numériques option A (Informatique et Réseaux) Niveau(x) S4. Développement logiciel

S4.6. Programmation orientée objet (Support : C++)

Classes abstraites, virtualité 3

Objectifs du TP :

- Ligature dynamique, fonction virtuelle,polymorphisme, classe abstraite et transtypage (cast).

Support d’activité :

- Logiciels : Visual Studio et/ou CodeBlocks, MagicDraw et suite bureautique - Fichier : Les fonctions virtuelles.PDF

- Ce document au format PDF

Vous rédigerez un compte-rendu numérique.

Pensez aux captures d’écran pour imager votre compte-rendu.

Sauvegardez votre travail régulièrement !

Des modifications peuvent exister selon la version du logiciel utilisée.

LIGATURE DYNAMIQUE ET FONCTION VIRTUELLE

Prenez connaissance par une première lecture du fichier intitulé « Les fonctions virtuelles.pdf » se trouvant dans le dossier « Support » de l’activité.

(2)

TYPAGE STATIQUE DES OBJETS

Les règles de compatibilité entre une classe de base et une classe dérivée permettent d’affecter à un pointeur sur une classe de base la valeur d’un pointeur sur une classe dérivée. Toutefois, par défaut, le type des objets pointés est défini lors de la compilation.

Par exemple, avec : class A

{...

public :

void fct (...) ...

};

class B : public A {...

public :

void fct (...) ...

};

A *pta;

B *ptb;

une affectation telle que pta = ptb est autorisée. Néanmoins, quel que soit le contenu de pta (autrement dit, quel que soit l’objet pointé par pta), pta->fct(...) appelle toujours la fonction fct de la classe A.

LES FONCTIONS VIRTUELLES

L’emploi des fonctions virtuelles permet d’éviter les problèmes inhérents au typage statique.

Lorsqu’une fonction est déclarée virtuelle (mot-clé virtual) dans une classe, les appels à une telle fonction ou à n’importe laquelle de ses redéfinitions dans des classes dérivées sont « résolus

» au moment de l’exécution, selon le type de l’objet concerné. On parle de typage dynamique des objets (ou de ligature dynamique des fonctions).

Par exemple, avec : class A

{...

public :

virtual void fct (...) ...

};

class B : public A {...

public :

void fct (...) ...

};

A *pta;

(3)

l’instruction pta->fct (...) appellera la fonction fct de la classe correspondant réellement au type de l’objet pointé par pta.

Il peut y avoir ligature dynamique même en dehors de l’utilisation de pointeurs.

Règles :

Le mot-clé virtual ne s’emploie qu’une fois pour une fonction donnée ; plus précisément, il ne doit pas accompagner les redéfinitions de cette fonction dans les classes dérivées.

Une méthode déclarée virtuelle dans une classe de base peut ne pas être redéfinie dans ses classes dérivées.

Une fonction virtuelle peut être surdéfinie (chaque fonction surdéfinie pouvant être ou ne pas être virtuelle).

Un constructeur ne peut pas être virtuel, un destructeur peut l’être.

Par sa nature même, le mécanisme de ligature dynamique est limité à une hiérarchie de classes ; souvent, pour qu’il puisse s’appliquer à toute une bibliothèque de classes, on sera amené à faire hériter toutes les classes de la bibliothèque d’une même classe de base.

LES FONCTIONS VIRTUELLES PURES

Une « fonction virtuelle pure » se déclare avec une initialisation à zéro, comme dans : virtual void affiche() = 0;

Lorsqu’une classe comporte au moins une fonction virtuelle pure, elle est considérée comme « abstraite », on ne peut plus instancier la classe, c’est-à-dire qu’il n’est plus possible de créer des objets de son type.

Une fonction déclarée virtuelle pure dans une classe de base peut ne pas être déclarée dans une classe dérivée et, dans ce cas, elle est à nouveau implicitement fonction virtuelle pure de cette classe dérivée.

EXERCICE 1

Soit le code C++ ci-dessous :

#include <iostream>

using namespace std;

class Cpoint {

protected:

int x, y;

public:

Cpoint(int abs = 0, int ord = 0) { x = abs; y = ord; } virtual void affiche()

(4)

{

cout << "Je suis un point \n";

cout << " mes coordonnees sont : " << x << " " << y << "\n";

} };

class Cpointcol : public Cpoint {

private:

short couleur;

public:

Cpointcol(int abs = 0, int ord = 0, short cl = 1) : Cpoint(abs, ord) {

couleur = cl;

}

void affiche() {

cout << "Je suis un point colore \n";

cout << " mes coordonnees sont : " << x << " " << y;

cout << " et ma couleur est : " << couleur << "\n";

} };

int main() {

Cpoint p(3, 5); Cpoint * adp = &p;

Cpointcol pc(8, 6, 2); Cpointcol * adpc = &pc;

adp->affiche(); adpc->affiche();// appel 1 cout << "---\n";

adp = adpc;

adp->affiche(); adpc->affiche();// appel 2 return 0;

}

Sans compiler le code ci-dessus ! Répondez aux questions ci-dessous.

Question 1

Dans la déclaration de la classe Cpoint :

Pourquoi les membres donnés x et y sont en visibilité protected ? Question 2

Pour l’appel 1 :

Sur quel type d’objet pointe adp et sur quel type d’objet pointe adpc ? Quelles fonctions sont appellées par adp et adpc ?

Pour l’appel 2 :

Sur quel type d’objet pointe adp et sur quel type d’objet pointe adpc ?

(5)

Quelles fonctions sont appellées par adp et adpc ? Question 3

Vous pouvez maintenant créer le projet (avec modularité). Testez le programme afin de vérifier vos réponses.

Donnez le résultat produit par le programme.

EXERCICE 2

Soit le code C++ ci-dessous :

#include <iostream>

using namespace std;

class Cpoint {

protected:

int x, y;

public:

Cpoint(int abs = 0, int ord = 0) { x = abs; y = ord; } virtual void identifie()

{

cout << "Je suis un point \n";

}

void affiche() {

identifie();

cout << "Mes coordonnees sont : " << x << " " << y << "\n";

} };

class Cpointcol : public Cpoint {

private:

short couleur;

public:

Cpointcol(int abs = 0, int ord = 0, int cl = 1) : Cpoint(abs, ord) {

couleur = cl;

}

void identifie() {

cout << "Je suis un point de couleur : " << couleur << "\n";

} };

int main() {

Cpoint p(3, 4);

Cpointcol pc(5, 9, 5);

(6)

p.affiche();

pc.affiche();

cout << "---\n";

Cpoint * adp = &p;

Cpointcol * adpc = &pc;

adp->affiche(); adpc->affiche();

cout << "---\n";

adp = adpc;

adp->affiche(); adpc->affiche();

return 0;

}

Dans la fonction affiche() de Cpoint, l’appel de identifie() fait l’objet d’une ligature dynamique puisque cette dernière fonction a été déclarée virtuelle.

Sans compiler le code ci-dessus ! Répondez aux questions ci-dessous.

Question 1

Donnez le résultat produit par le programme.

Question 2

Supposons que vous enleviez la ligature dynamique.

Donnez alors le résultat produit par le programme.

Question 3

Vous pouvez maintenant créer le projet (avec modularité). Testez le code afin de vérifier vos réponses.

Donnez le résultat produit par le programme (avec et sans ligature dynamique) puis faîtes une conclusion.

EXERCICE 3

Le polymorphisme est un moyen de manipuler des objets hétéroclites de la même manière, pourvu qu'ils disposent d'une interface commune. Un objet polymorphe est un objet susceptible de prendre plusieurs formes pendant l'exécution. Le polymorphisme représente la capacité du

système à choisir dynamiquement la méthode qui correspond au type de l'objet en cours de manipulation. Le polymorphisme est implémenté en C++ avec les fonctions virtuelles (virtual) et l'héritage.

Soit le code C++ ci-dessous :

#include <iostream>

using namespace std;

class Cforme

(7)

{

public:

Cforme() { cout << "constructeur Forme : "; }

void dessiner() { cout << "je dessine ... une forme ?\n"; } };

class Ccercle : public Cforme {

public:

Ccercle() { cout << "Cercle\n"; }

void dessiner() { cout << "je dessine un Cercle !\n"; } };

class Ctriangle : public Cforme {

public:

Ctriangle() { cout << "Triangle\n"; }

void dessiner() { cout << "je dessine un Triangle !\n"; } };

void faireQuelqueChose(Cforme &f) {

f.dessiner(); // dessine une Forme }

int main() {

Ccercle c;

Ctriangle t;

faireQuelqueChose(c); // avec un cercle faireQuelqueChose(t); // avec un triangle

return 0;

}

Sans compiler le code ci-dessus ! Répondez aux questions ci-dessous.

Question 1

À votre avis, obtenons nous un comportement polymorphe ? Justifiez votre réponse.

Question 2

Donnez le résultat produit par le programme.

Question 3

Vous pouvez maintenant créer le projet (avec modularité). Testez le code afin de vérifier vos réponses.

Donnez le résultat produit par le programme.

(8)

Soit le code C++ ci-dessous :

#include <iostream>

using namespace std;

class Cforme { public:

Cforme() { cout << "constructeur Forme : "; }

virtual void dessiner() { cout << "je dessine ... une forme ?\n"; } };

class Ccercle : public Cforme { public:

Ccercle() { cout << "Cercle\n"; }

void dessiner() { cout << "je dessine un Cercle !\n"; } };

class Ctriangle : public Cforme { public:

Ctriangle() { cout << "Triangle\n"; }

void dessiner() { cout << "je dessine un Triangle !\n"; } };

void faireQuelqueChose(Cforme &f) {

f.dessiner(); // dessine une Forme }

int main() {

Ccercle c;

Ctriangle t;

faireQuelqueChose(c);

faireQuelqueChose(t);

return 0;

}

Sans compiler le code ci-dessus ! Répondez aux questions ci-dessous.

Question 4

À votre avis, obtenons nous un comportement polymorphe ? Justifiez votre réponse.

Question 5

Donnez le résultat produit par le programme.

Question 6

Vous pouvez maintenant créer le projet (avec modularité). Testez le code afin de vérifier vos réponses.

Donnez le résultat produit par le programme.

(9)

Question 7

Expliquez les lignes de code ci-dessous :

class Ctriangle : public Cforme

void faireQuelqueChose(Cforme &f) {

f.dessiner(); // dessine une Forme }

faireQuelqueChose(c);

En utilisant l'héritage, il est possible : - d'ajouter des caractéristiques ;

- d'utiliser les caractéristiques héritées ;

- de redéfinir les comportements (méthodes héritées).

Il ne faut pas confondre la redéfinition (overriding) et la surdéfinition ou surcharge (overloading) :

Une surdéfinition (ou surcharge) permet d'utiliser plusieurs méthodes qui portent le même nom au sein d'une même classe avec une signature différente.

Une redéfinition (overriding) permet de fournir une nouvelle définition d'une méthode d'une classe ascendante pour la remplacer. Elle doit avoir une signature rigoureusement

identique à la méthode parente.

EXERCICE 4

Soit le code C++ ci-dessous :

#include <iostream>

class A { private:

int *p;

public:

A() { p = new int[4]; std::cout << "A() et p=" << p; } ~A() { delete[] p; std::cout << " \n~A()"; }

};

class B : public A { private:

int *q;

public:

B() { q = new int[64]; std::cout << "\nB() et q=" << q; } ~B() { delete[] q; std::cout << "\n~B()"; }

};

int main() { A *pA = new B();

(10)

delete pA;

return 0;

}

Question 1

Le code ci-dessus fait apparaître à l’affichage un problème de fuite de mémoire.

Expliquez le problème rencontré.

À FAIRE VALIDER PAR LE PROFESSEUR, AVANT DE POURSUIVRE ! Question 2

Essayez de résoudre le problème en modifiant le code précédent.

EXERCICE 5

Soit le code C++ contenant quelques erreurs ci-dessous :

#include <iostream>

using namespace std;

class CAnimal {

public:

CAnimal(int nAge = 0) {

this->m_nAge = nAge;

this->bDomestique = false;

cout << "Nouvel animal ";

}

~CAnimal() { cout << " Mort de l'animal" << endl; } int GetAge() const { return this->m_nAge; }

void Crier() const { cout << "Cri de votre animal " << endl; } void Manger() const { cout << "eau "; }

private:

int m_nAge;

protected:

bool bDomestique;

};

class COiseau : public CAnimal {

public:

COiseau() { cout << " Nouvel oiseau" << endl; } ~COiseau() { cout << " Mort de l'oiseau" << endl; }

void Crier() const { cout << "Cuit cuit...." << endl; }

void Manger() const { CAnimal::Manger(); cout << "graine" << endl; } };

class CCheval : public CAnimal {

public:

CCheval(int nAge) : CAnimal(nAge) {

this->m_nAge = nAge;

this->bDomestique = true;

(11)

cout << "Nouveau cheval";

}

~CCheval() { cout << " Mort du cheval" << endl; } void Crier() const { cout << "Hiiii!...." << endl; }

void Manger() const { CAnimal::Manger(); cout << "avoine" << endl; } };

int main() {

int race;

CAnimal anim(2);

anim.domestique = true;

anim.Crier();

CCheval tornado(10);

tornado.Crier();

tornado.Manger();

cout << "tornado a " << tornado.GetAge() << " ans" << endl;

COiseau titi;

titi.Crier();

COiseau *pCOiseau = new COiseau();

pCOiseau->Crier();

delete pCOiseau;

CAnimal *panimal;

cout << "CCheval(1) ou COiseau (2) : ";

cin >> race;

if (race == 1)

panimal = new CCheval(10);

else if (race == 2)

panimal = new COiseau();

panimal->Crier();

panimal->Manger();

return 0;

}

Sans compiler le code ci-dessus ! Répondez aux questions ci-dessous.

Question 1

Analysez le code.

Expliquez pourquoi les deux lignes de code ci-dessous poseront problème lors de la compilation.

public:

CCheval(int nAge) : CAnimal(nAge) {

this ->m_nAge = nAge; // Problème lors de la compilation !!!

this->bDomestique = true;

cout << "Nouveau cheval";

}

int main() {

int race;

(12)

CAnimal anim(2);

anim.domestique = true; // Problème lors de la compilation !!!

Question 2

Créez le projet avec modularité.

Mettez les deux lignes de code posant problème en commentaire.

Compilez puis exécutez le programme.

Que constatez-vous concernant le résultat de l’appel ci-dessous : panimal->Crier();

Question 3

Modifiez le programme de façon à ce que panimal->Crier() appelle la bonne fonction.

Question 4

À l’aide du logiciel MagicDraw :

Représentez le diagramme de classes du programme précédent.

EXERCICE 6 (PLUS DIFFICILE)

Quelquefois, la modélisation d'un concept très général conduit à laisser des « trous » dans l'implémentation. Une classe abstraite permet d'introduire certaines méthodes dont on ne peut encore donner aucune définition.

Par exemple, on ne sait pas programmer ce que doit faire la fonction dessiner() dans le contexte de Forme. On veut juste s'assurer de sa présence dans toutes les classes filles, sans devoir la définir dans la classe parente.

Une classe est dite abstraite si elle contient au moins une fonction virtuelle pure. Une fonction membre est dite virtuelle pure lorsqu'elle est déclarée de la façon suivante :

virtual type nomMethode(paramètres) = 0;

On ne peut pas instancier d'objet à partir d'une classe abstraite. Mais on le peut à partir d'une classe dérivée à condition qu'elle définisse complètement la méthode virtuelle pure.

On souhaite créer une classe nommée Cexo permettant de manipuler des ensembles dont le type des éléments est non seulement inconnu de la classe, mais également susceptible de varier d’un élément à un autre. Pour que la chose soit possible, on imposera simplement la contrainte

(13)

suivante : tous les types concernés devront dériver d’un même type de base nommé Cbase. Le type base sera supposé connu au moment où l’on définit la classe Cexo.

La classe Cbase disposera au moins d’une fonction virtuelle pure nommée affiche() ; cette fonction devra être redéfinie dans les classes dérivées pour afficher les caractéristiques de l’objet concerné.

La classe Cexo disposera des fonctions membre suivantes :

ajoute() pour ajouter un nouvel élément à l’ensemble (elle devra s’assurer qu’il n’existe pas déjà) ;

appartient() pour tester l’appartenance d’un élément à l’ensemble ;

• cardinal() qui fournira le nombre d’éléments de l’ensemble.

De plus, la classe devra être munie d’un « itérateur », c’est-à-dire d’un mécanisme permettant de parcourir les différents éléments de l’ensemble. On prévoira 3 fonctions :

• init() pour initialiser le mécanisme d’itération ;

suivant() qui fournira en retour le prochain élément (objet d’un type dérivé de base) ;

• existe() pour préciser s’il existe encore un élément non examiné.

Enfin, une fonction nommée liste() permettra d’afficher les caractéristiques de tous les éléments de l’ensemble (elle fera, bien sûr, appel aux fonctions affiche() des différents objets concernés).

On réalisera ensuite un petit programme d’essai de la classe Cexo, en créant un ensemble comportant des objets de type Cpoint (deux coordonnées entières) et Ccomplexe (une partie réelle et une partie imaginaire, toutes deux de type float). Naturellement, Cpoint et Ccomplexe devront dériver de base. On ne se préoccupera pas des éventuels problèmes posés par l’affectation ou la transmission par valeur d’objets du type Cexo.

Pour ne pas trop compliquer le programme, on fondera l’égalité de deux éléments d’un ensemble sur l’égalité de leurs adresses et non pas de leurs valeurs. Autrement dit, deux éléments de même type et de même valeur pourront appartenir à un même ensemble, à condition qu’il s’agisse bien de deux objets différents.

Ci-dessous, un exemple d’affichage console attendu pour trois objets créés : Cpoint p (1,3) ;

Ccomplexe z (0.5, 3) ; Cexo e ;

(14)

Question 1

À vous de le faire ! Question 2

À l’aide du logiciel MagicDraw :

Représentez le diagramme de classes du programme précédent.

Nommage du compte-rendu : VOTRE NOM_SNIR2_TP_C++_Virtuelle.pdf

Références

Documents relatifs

Il n’est donc pas nécessaire d’invoquer explicitement ce constructeur depuis celui de la sous-classe RectanglePlein , et il n’est donc pas non plus obligatoire de fournir

[r]

[r]

[r]

[r]

Et ceux qui s'enfermeraient dans l'édüice aveuglé, caressant de leurs mains des murs froids et rudes et des outils inertes, ceux qui refuseraient de prendre le recul

-Association de tailles similaires par manipulation (individuellement) : remettre les poupées de tailles différentes dans leur lit (6) , reconstituer un dessin avec des

Consigne : Classe le personnage de Violette du plus grand au plus petit... Consigne : Classe le personnage de Violette du plus petit au