Polymorphisme en C++
Exercice 1 - Concepts de base du polymorphisme Question 1 - Résultat de l'instanciation de B Le code suivant void main(){ B obj; } crée une instance locale de la classe B . L'ordre d'appel des constructeurs et des destructeurs en C++ obéit à des règles strictes : Le constructeur de la classe de base ( A ) est appelé avant celui de la classe dérivée ( B ).
D'après le document Polymorphisme en C++
Cet article a été rédigé automatiquement à partir du document source, puis vérifié avant publication.

Document source
Programming · École Nationale des Sciences de l'Informatique (ENSI) · PDF · 3 pages · 2016
Afficher l'aperçu du document
Exercice 1 - Concepts de base du polymorphisme
Question 1 - Résultat de l'instanciation de B
Le code suivant void main(){ B obj; } crée une instance locale de la classe B.
L'ordre d'appel des constructeurs et des destructeurs en C++ obéit à des règles strictes :
- Le constructeur de la classe de base (
A) est appelé avant celui de la classe dérivée (B). - À la fin du bloc (fin de la fonction
main), l'objet est détruit. Les destructeurs sont appelés dans l'ordre inverse des constructeurs : d'abord celui deB, puis celui deA.
Le résultat de l'affichage sera donc :
A::A()
B::B()
B:: ~B()
A:: ~A()
Question 2 - Différence de convention d'appel
La différence fondamentale se situe au niveau de l'édition des liens (binding) :
- Méthode non virtuelle : Elle utilise une résolution statique (early binding). L'adresse de la fonction à appeler est déterminée à la compilation en se basant sur le type du pointeur ou de la référence, et non sur le type réel de l'objet pointé.
- Méthode virtuelle : Elle utilise une résolution dynamique (late binding). Le choix de la fonction à exécuter est retardé jusqu'à l'exécution. Le programme consulte une table des méthodes virtuelles (la vtable) propre à l'objet réel en mémoire pour appeler la méthode de la classe la plus dérivée.
Question 3 - Analyse du comportement des pointeurs
Analysons ligne par ligne le code proposé :
B *pB = new B;instancie un objetB. Affichage :A::A()puisB::B().A *pA = pB;crée un pointeur de typeA*pointant vers notre objetB. Aucun affichage.pA->f1();appellef1(). Commef1()est déclaréevirtualdansA, le programme regarde le type réel de l'objet (qui estB). La méthode redéfinie dansBest exécutée. Affichage :B::f1().pB->f2();appellef2()via un pointeurB*. DansB,f2()masque celle deA. Affichage :B::f2().pB->f1();appellef1()via un pointeurB*. Affichage :B::f1().pA->f2();appellef2()via un pointeurA*. La méthodef2()n'est pas virtuelle dans la classeA. La résolution est donc statique basée sur le type du pointeur (A*). C'est la méthode deAqui est appelée, même si l'objet est unB. Affichage :A::f2().
(Note : le programme ne fait pas de delete pB;, donc les destructeurs ne sont pas appelés).
Résultat complet à l'écran :
A::A()
B::B()
B::f1()
B::f2()
B::f1()
A::f2()
Exercice 2 - Double dispatch et résolution de surcharges
Question 1 - Arborescence des classes
Il s'agit d'une arborescence d'héritage simple où une classe abstraite de base est spécialisée par deux classes dérivées :
- La classe
Figureest la classe de base (classe mère). - Les classes
CercleetRectanglehéritent publiquement deFigure(classes filles).
Question 2 - Trace d'exécution du programme
Ce code illustre le motif de conception Double Dispatch (ou un visiteur simplifié). La résolution des surcharges en C++ se fait à la compilation sur le type statique des arguments, mais le polymorphisme s'applique à l'objet appelant (this).
-
c1.intersection(c2);Les deux objets sont statiquement et dynamiquement de typeCercle. L'appel direct àCercle::intersection(const Cercle&)est résolu. Affichage Ligne 1 :Inter. cercle C1 et cercle C2 -
ptr1->intersection(r1);ptr1est un pointeur deFigurepointant vers unCercle(nommé "C3"). L'argumentr1est statiquement unRectangle. L'appel virtuel utilise la redéfinitionCercle::intersection(const Rectangle&). Affichage Ligne 2 :Inter. cercle C3 et rectangle R1 -
c2.intersection(*ptr2);c2est unCercle. L'argument*ptr2est passé via le type statiqueFigure&(car le pointeur est de typeFigure*). Le compilateur sélectionne doncCercle::intersection(const Figure& f). Dans cette méthode, on exécutef.intersection(*this). Ici,f(l'objet réel est unRectanglenommé "R3") appelle sa méthode virtuelle avec*this(qui est statiquement unCercledepuis l'intérieur de la classeCercle). Le polymorphisme renvoie versRectangle::intersection(const Cercle&). Affichage Ligne 3 :Inter. rectangle R3 et cercle C2 -
ptr2->intersection(*ptr1);Le type statique de*ptr1estFigure&. L'appel surptr2(réellement unRectangle) tombe dansRectangle::intersection(const Figure& f). Dans cette méthode,f.intersection(*this)est exécuté.fest réellement unCercle, et*thisest statiquement unRectangle. Le polymorphisme résout ceci versCercle::intersection(const Rectangle&). Affichage Ligne 4 :Inter. cercle C3 et rectangle R3
Exercice 3 - Cycle de vie et constructeurs
Note de correction : le texte source de cet exercice a été corrompu lors de l'extraction PDF (mélange des lignes de constructeurs). Le code réparé logiquement attribue un membre Process p; à Thread et nettoie les initialisations de constructeurs d'après les traces cout.
Question 1 - Analyse de l'affichage par instruction
Avant même d'entrer dans main(), la variable globale Process obj_pro1; est instanciée :
Affichage global : Process Defaut
Voici ensuite le comportement instruction par instruction dans main() :
| Instruction | Affichage généré | Explication |
|---|---|---|
Thread obj_thr1(2); |
Process Param<br>Process Defaut<br>Thread Param |
La création de Thread appelle d'abord le constructeur parent Process(2), puis le constructeur par défaut de son membre Process p, puis le corps de son propre constructeur. |
Process *ptr_pro = new Process [2]; |
Process Defaut<br>Process Defaut |
Un tableau de 2 objets Process est alloué. Le constructeur par défaut est appelé pour chacun d'eux. |
ptr_pro[0].imprimer(); |
pid=0 |
La méthode imprimer() affiche le pid, initialisé à 0 par défaut. |
delete [] ptr_pro; |
Process Destructeur<br>Process Destructeur |
Les deux objets du tableau sont détruits. |
obj_thr1.imprimer(); |
tid=1 |
Affiche le tid du Thread, initialisé à 1 dans son constructeur. |
ptr_pro = &obj_thr1; |
(Aucun) | Simple assignation de pointeur. |
ptr_pro->imprimer(); |
tid=1 |
Appel polymorphe. ptr_pro pointe vers un Thread, donc Thread::imprimer() est exécuté. |
obj_thr1.Process::imprimer(); |
pid=2 |
L'opérateur de résolution de portée force l'appel de la méthode de la classe mère. |
Process obj_pro2(obj_pro1); |
Process Copie |
Utilisation du constructeur de recopie de Process. |
obj_pro2.imprimer(); |
pid=0 |
Copie de obj_pro1 (qui avait pid = 0). |
Fonction(obj_thr1); |
Process Copie<br>Process Copie<br>tid=1<br>Thread Destructeur<br>Process Destructeur<br>Process Destructeur |
Le passage par valeur à Fonction copie obj_thr1. En l'absence de constructeur de recopie spécifique dans Thread, le compilateur en génère un qui appelle les constructeurs de recopie du parent (Process) et du membre (p). La fonction appelle ensuite x.imprimer() (tid=1). À la fin de la fonction, la variable locale x est détruite (destructeur parent, membre, puis base). |
return 0; |
Process Destructeur<br>Thread Destructeur<br>Process Destructeur<br>Process Destructeur |
Fin du main. Destruction des variables locales dans l'ordre inverse : obj_pro2, puis obj_thr1 (qui détruit le Thread, son membre Process, puis sa base Process). À la toute fin du programme, la variable globale obj_pro1 est aussi détruite (un Process Destructeur supplémentaire). |
Question 2 - Ajout du constructeur de recopie à Thread
Pour que la classe Thread affiche un message de copie tout en maintenant la sémantique de copie correcte de son héritage et de ses membres, nous devons définir le constructeur ainsi :
Thread(Thread& t) : Process(t), p(t.p) {
tid = t.tid;
cout << "Thread Copie \n";
}
Avec cet ajout, lors de l'exécution de l'instruction Fonction(obj_thr1);, l'ordre d'initialisation sera respecté (base, membres, constructeur) mais le constructeur de recopie de Thread prendra la main. Le nouvel affichage de cette seule ligne deviendra :
Process Copie
Process Copie
Thread Copie
tid=1
Thread Destructeur
Process Destructeur
Process Destructeur
Exercice 4 - Modélisation de véhicules en C++
Question 1 - Interface et implémentation des véhicules
Nous définissons une classe de base abstraite Vehicule et ses classes dérivées. Pour éviter de répéter le code, nous utilisons les fonctionnalités de l'héritage pour affichage().
#include <iostream>
#include <string>
using namespace std;
class Vehicule {
protected:
string immatriculation;
float poids_a_vide;
public:
Vehicule(string imm, float poids) : immatriculation(imm), poids_a_vide(poids) {}
virtual ~Vehicule() {}
// Méthode virtuelle pure : chaque véhicule doit calculer sa vitesse max
virtual float vitesse_max() const = 0;
virtual void affichage() const {
cout << "Immatriculation: " << immatriculation
<< " | Poids a vide: " << poids_a_vide << " t";
}
// Le prototype demandé utilise des int.
// Attention à diviser par 10.0 pour éviter la division entière.
float consommation(int v, int p) const {
return (v / 10.0f) + p;
}
};
class Petit_bus : public Vehicule {
public:
Petit_bus(string imm) : Vehicule(imm, 4.0f) {}
float vitesse_max() const override {
return 150.0f;
}
void affichage() const override {
Vehicule::affichage();
cout << " | Type: Petit Bus | Vitesse Max: " << vitesse_max() << " km/h" << endl;
}
};
class Camion_citerne : public Vehicule {
private:
float charge;
public:
Camion_citerne(string imm, float ch = 0.0f) : Vehicule(imm, 3.0f), charge(ch) {
if (charge > 10.0f) charge = 10.0f; // Bridage à la charge max
}
float vitesse_max() const override {
if (charge == 0.0f) return 130.0f;
if (charge <= 1.0f) return 110.0f;
if (charge <= 4.0f) return 90.0f;
return 80.0f;
}
void affichage() const override {
Vehicule::affichage();
cout << " | Type: Citerne | Charge: " << charge
<< " t | Vitesse Max: " << vitesse_max() << " km/h" << endl;
}
};
class Camion_bache : public Vehicule {
private:
float charge;
public:
Camion_bache(string imm, float ch = 0.0f) : Vehicule(imm, 4.0f), charge(ch) {
if (charge > 20.0f) charge = 20.0f;
}
float vitesse_max() const override {
if (charge == 0.0f) return 130.0f;
if (charge <= 3.0f) return 110.0f;
if (charge <= 7.0f) return 90.0f;
return 80.0f;
}
void affichage() const override {
Vehicule::affichage();
cout << " | Type: Bache | Charge: " << charge
<< " t | Vitesse Max: " << vitesse_max() << " km/h" << endl;
}
};
Question 2 - Interface et implémentation du Convoi
La classe Convoi gère une collection de pointeurs vers la classe de base Vehicule (polymorphisme) via un std::vector.
#include <vector>
class Convoi {
private:
vector<Vehicule*> vehicules;
public:
Convoi() {}
void ajouter(Vehicule* v) {
vehicules.push_back(v);
}
// Supprime le dernier véhicule du convoi
void supprimer() {
if (!vehicules.empty()) {
vehicules.pop_back();
}
}
float vitesse_max() const {
if (vehicules.empty()) return 0.0f;
float min_vitesse = vehicules[0]->vitesse_max();
for (size_t i = 1; i < vehicules.size(); ++i) {
float vitesse_courante = vehicules[i]->vitesse_max();
if (vitesse_courante < min_vitesse) {
min_vitesse = vitesse_courante;
}
}
return min_vitesse;
}
void afficher_convoi() const {
cout << "--- CONVOI DE " << vehicules.size() << " VEHICULES ---" << endl;
for (Vehicule* v : vehicules) {
v->affichage();
}
cout << ">> VITESSE MAX DU CONVOI : " << vitesse_max() << " km/h" << endl;
}
};
Question 3 - Programme d'utilisation
Voici le code principal répondant strictement aux étapes a) à f) du sujet :
int main() {
// a) créer un convoi 'C'
Convoi C;
// b) créer deux camions bâchés
Camion_bache* cb1 = new Camion_bache("CB-001", 2.0f);
Camion_bache* cb2 = new Camion_bache("CB-002", 5.0f);
// c) créer un petit bus
Petit_bus* pb1 = new Petit_bus("PB-001");
// d) créer trois camions citerne
Camion_citerne* cc1 = new Camion_citerne("CC-001", 0.0f);
Camion_citerne* cc2 = new Camion_citerne("CC-002", 3.0f);
Camion_citerne* cc3 = new Camion_citerne("CC-003", 8.0f);
// e) ajouter ces véhicules à 'C'
C.ajouter(cb1);
C.ajouter(cb2);
C.ajouter(pb1);
C.ajouter(cc1);
C.ajouter(cc2);
C.ajouter(cc3);
// f) afficher les descriptions et la vitesse maximale de chaque véhicule de 'C'
C.afficher_convoi();
// (Nettoyage de la mémoire omis pour brièveté de la réponse)
return 0;
}
Méthode
Face à un examen sur le polymorphisme en C++, voici les points d'attention cruciaux :
- Tracez les appels avec méthode : Ne regardez jamais la fonction par son nom seul. Posez-vous deux questions systématiquement : "Quel est le type statique (le pointeur/la référence) ?" et "La méthode appelée est-elle
virtual?". Si c'est oui, allez chercher le type dynamique (le vrai objet en mémoire). Sinon, utilisez le type statique. - Double Dispatch : Dans des exercices comme celui des intersections (Exercice 2), retenez que
thisa toujours un type statique très précis : il correspond à la classe dans laquelle vous écrivez le code. C'est ce qui permet de forcer la résolution de la bonne surcharge lors du deuxième appel virtuel. - Le cycle de vie : Un objet créé est un objet qui doit être détruit. N'oubliez pas les variables membres qui ont leurs propres constructeurs/destructeurs exécutés "en cascade", ni les copies temporaires créées par un passage par valeur à une fonction. Un bout de brouillon avec des flèches temporelles aide grandement sur ce type d'exercice (Exercice 3).
Commentaires
Aucun commentaire pour le moment. Posez la première question.