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.

Polymorphisme en C++

Document source

Polymorphisme en C++

Programming · École Nationale des Sciences de l'Informatique (ENSI) · PDF · 3 pages · 2016

Afficher l'aperçu du document

Consulter le document original →

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 :

  1. Le constructeur de la classe de base (A) est appelé avant celui de la classe dérivée (B).
  2. À 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 de B, puis celui de A.

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 objet B. Affichage : A::A() puis B::B().
  • A *pA = pB; crée un pointeur de type A* pointant vers notre objet B. Aucun affichage.
  • pA->f1(); appelle f1(). Comme f1() est déclarée virtual dans A, le programme regarde le type réel de l'objet (qui est B). La méthode redéfinie dans B est exécutée. Affichage : B::f1().
  • pB->f2(); appelle f2() via un pointeur B*. Dans B, f2() masque celle de A. Affichage : B::f2().
  • pB->f1(); appelle f1() via un pointeur B*. Affichage : B::f1().
  • pA->f2(); appelle f2() via un pointeur A*. La méthode f2() n'est pas virtuelle dans la classe A. La résolution est donc statique basée sur le type du pointeur (A*). C'est la méthode de A qui est appelée, même si l'objet est un B. 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 Figure est la classe de base (classe mère).
  • Les classes Cercle et Rectangle héritent publiquement de Figure (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 type Cercle. L'appel direct à Cercle::intersection(const Cercle&) est résolu. Affichage Ligne 1 : Inter. cercle C1 et cercle C2

  • ptr1->intersection(r1); ptr1 est un pointeur de Figure pointant vers un Cercle (nommé "C3"). L'argument r1 est statiquement un Rectangle. L'appel virtuel utilise la redéfinition Cercle::intersection(const Rectangle&). Affichage Ligne 2 : Inter. cercle C3 et rectangle R1

  • c2.intersection(*ptr2); c2 est un Cercle. L'argument *ptr2 est passé via le type statique Figure& (car le pointeur est de type Figure*). Le compilateur sélectionne donc Cercle::intersection(const Figure& f). Dans cette méthode, on exécute f.intersection(*this). Ici, f (l'objet réel est un Rectangle nommé "R3") appelle sa méthode virtuelle avec *this (qui est statiquement un Cercle depuis l'intérieur de la classe Cercle). Le polymorphisme renvoie vers Rectangle::intersection(const Cercle&). Affichage Ligne 3 : Inter. rectangle R3 et cercle C2

  • ptr2->intersection(*ptr1); Le type statique de *ptr1 est Figure&. L'appel sur ptr2 (réellement un Rectangle) tombe dans Rectangle::intersection(const Figure& f). Dans cette méthode, f.intersection(*this) est exécuté. f est réellement un Cercle, et *this est statiquement un Rectangle. Le polymorphisme résout ceci vers Cercle::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 :

  1. 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.
  2. Double Dispatch : Dans des exercices comme celui des intersections (Exercice 2), retenez que this a 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.
  3. 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).

Partager

Commentaires

Aucun commentaire pour le moment. Posez la première question.

Les commentaires sont relus avant publication. Votre e-mail n'est jamais affiché.

← Toutes les révisions