Retour au cours

Cours sur Héritage, constructeurs et polymorphisme en C++

Appartient au cours : Héritage, constructeurs et polymorphisme en C++

Programmation Orientée Objet : Héritage, Polymorphisme et Conteneurs STL

Chapitre 1 : L'Héritage en POO

Objectifs d'Apprentissage 🎯

  • Comprendre le mécanisme d'héritage et ses objectifs en POO.
  • Maîtriser la terminologie associée (classe mère/fille, base/dérivée).
  • Connaître les règles d'accès aux membres hérités.
  • Distinguer les différents types d'héritage (public, protected, private).

1.1 Définition et Objectifs

L'héritage est un mécanisme fondamental de la POO permettant de créer une nouvelle classe (dite fille ou dérivée) à partir d'une classe existante (dite mère ou base). Ce mécanisme permet :

  • La réutilisation du code : La classe fille hérite des attributs et méthodes de la classe mère.
  • La spécialisation : La classe fille peut ajouter de nouveaux membres ou redéfinir des méthodes existantes.
  • La hiérarchie logique : Organisation des classes en une structure arborescente reflétant des relations de type "est-un" (ex: un Chien est un Animal).
classDiagram
    Animal <|-- Chien
    class Animal {
        +manger()
        +dormir()
    }
    class Chien {
        +aboyer()
    }

1.2 Règles d'Accès aux Membres Hérités

Les membres d'une classe mère peuvent avoir trois niveaux de visibilité, qui déterminent leur accessibilité dans la classe fille :

Niveau de VisibilitéAccessible dans la Classe Fille ?Accessible depuis l'Extérieur ?
private❌ Non❌ Non
protected✅ Oui❌ Non
public✅ Oui✅ Oui

Notes de l'IA 🧑‍🏫 :

  • Les membres private d'une classe mère sont strictement réservés à cette classe. Même une classe fille ne peut y accéder directement. Pour permettre un accès contrôlé, utilisez protected.
  • Le mot-clé protected est souvent utilisé pour les attributs ou méthodes qui doivent être accessibles aux classes dérivées, mais pas au reste du programme.

1.3 Types d'Héritage

Le type d'héritage détermine comment les niveaux de visibilité des membres de la classe mère sont modifiés dans la classe fille :

Type d'HéritageEffet sur les Membres de la Classe Mère
publicLes membres public/protected restent inchangés.
protectedLes membres public/protected deviennent protected.
privateLes membres public/protected deviennent private.

Exemple en C++ :

class Animal {
public:
    void manger();
protected:
    void dormir();
private:
    int age;
};

// Héritage public
class Chien : public Animal {
    // manger() reste public
    // dormir() reste protected
    // age reste inaccessible
};

// Héritage protected
class Chat : protected Animal {
    // manger() devient protected
    // dormir() reste protected
    // age reste inaccessible
};

// Héritage private
class Oiseau : private Animal {
    // manger() devient private
    // dormir() devient private
    // age reste inaccessible
};

Points Clés à Retenir 📌

  • L'héritage permet de réutiliser et spécialiser le code existant.
  • Une classe fille hérite de tous les membres de la classe mère, mais leur accessibilité dépend de leur niveau de visibilité initial et du type d'héritage.
  • Les membres private d'une classe mère sont toujours inaccessibles à la classe fille.
  • L'héritage public est le plus couramment utilisé, car il préserve la relation "est-un".

Chapitre 2 : Héritage et Constructeurs

Objectifs d'Apprentissage 🎯

  • Comprendre l'ordre d'appel des constructeurs dans une hiérarchie d'héritage.
  • Savoir initialiser les attributs hérités via le constructeur de la classe mère.
  • Maîtriser l'utilisation de la liste d'initialisation pour appeler un constructeur spécifique de la classe mère.

2.1 Appel en Cascade des Constructeurs

Lors de la création d'un objet d'une classe dérivée, les constructeurs sont appelés dans un ordre hiérarchique :

  1. Constructeur de la classe mère (de la plus générale à la plus spécifique).
  2. Constructeur de la classe fille.

Exemple :

class Animal {
public:
    Animal() { std::cout << "Constructeur Animal\n"; }
};

class Chien : public Animal {
public:
    Chien() { std::cout << "Constructeur Chien\n"; }
};

int main() {
    Chien monChien; // Affiche : "Constructeur Animal" puis "Constructeur Chien"
    return 0;
}

2.2 Initialisation des Attributs Hérités

Les attributs hérités de la classe mère sont initialisés par le constructeur de la classe mère. Il est donc crucial de bien définir les constructeurs de la classe mère pour garantir une initialisation correcte.

Notes de l'IA 🧑‍🏫 :

  • Si la classe mère n'a pas de constructeur par défaut (sans paramètres), vous devez appeler explicitement un constructeur de la classe mère dans la liste d'initialisation de la classe fille.
  • Oublier d'appeler le constructeur de la classe mère lorsque celui-ci n'a pas de constructeur par défaut entraîne une erreur de compilation.

2.3 Choix du Constructeur de la Classe Mère

Pour appeler un constructeur spécifique de la classe mère, utilisez la liste d'initialisation dans le constructeur de la classe fille.

Exemple :

class Personne {
public:
    Personne(std::string nom, int age) : nom_(nom), age_(age) {}
private:
    std::string nom_;
    int age_;
};

class Etudiant : public Personne {
public:
    Etudiant(std::string nom, int age, int numeroEtudiant)
        : Personne(nom, age), numeroEtudiant_(numeroEtudiant) {}
private:
    int numeroEtudiant_;
};

Points Clés à Retenir 📌

  • Les constructeurs sont appelés dans un ordre hiérarchique, de la classe mère à la classe fille.
  • Les attributs hérités sont initialisés par le constructeur de la classe mère.
  • Utilisez la liste d'initialisation pour appeler un constructeur spécifique de la classe mère.
  • Si la classe mère n'a pas de constructeur par défaut, vous devez appeler explicitement un constructeur de la classe mère.

Chapitre 3 : Le Masquage de Méthodes

Objectifs d'Apprentissage 🎯

  • Comprendre le mécanisme de masquage des méthodes en POO.
  • Savoir quand et pourquoi masquer une méthode de la classe mère.
  • Connaître l'impact du type statique sur l'appel des méthodes masquées.
  • Appliquer les bonnes pratiques pour éviter la duplication de code.

3.1 Définition et Mécanisme

Le masquage (ou hiding) se produit lorsqu'une classe fille définit une méthode portant le même nom qu'une méthode de la classe mère. Dans ce cas :

  • La méthode de la classe mère est masquée pour les objets de la classe fille.
  • La méthode appelée dépend du type statique de la variable (et non du type réel de l'objet).

Exemple :

class Animal {
public:
    void afficher() { std::cout << "Animal\n"; }
};

class Chien : public Animal {
public:
    void afficher() { std::cout << "Chien\n"; }
};

int main() {
    Chien monChien;
    Animal* ptrAnimal = &monChien;
    
    monChien.afficher();    // Affiche "Chien" (type statique : Chien)
    ptrAnimal->afficher();  // Affiche "Animal" (type statique : Animal*)
    
    return 0;
}

3.2 Raison d'Être du Masquage

Le masquage permet de personnaliser le comportement d'une classe fille tout en conservant la structure héritée de la classe mère. Cela est particulièrement utile pour :

  • Adapter une méthode aux besoins spécifiques de la classe fille.
  • Ajouter des fonctionnalités tout en réutilisant le code de la classe mère.

3.3 Bonnes Pratiques

Pour éviter la duplication de code et garantir une bonne maintenabilité, il est recommandé de :

  1. Appeler la méthode de la classe mère depuis la méthode masquée de la classe fille.
  2. Ajouter les spécificités de la classe fille après cet appel.

Exemple de Bonne Pratique :

class Animal {
public:
    void afficher() { std::cout << "Je suis un animal. "; }
};

class Chien : public Animal {
public:
    void afficher() {
        Animal::afficher(); // Appel de la méthode de la classe mère
        std::cout << "Plus précisément, je suis un chien.\n";
    }
};

Notes de l'IA 🧑‍🏫 :

  • Le masquage n'est pas du polymorphisme. Pour activer le polymorphisme, il faut utiliser des méthodes virtual (voir Chapitre 4).
  • Le masquage peut entraîner des comportements inattendus si le type statique de la variable ne correspond pas au type réel de l'objet. Soyez vigilant !

Points Clés à Retenir 📌

  • Le masquage se produit lorsqu'une classe fille définit une méthode de même nom qu'une méthode de la classe mère.
  • La méthode appelée dépend du type statique de la variable, pas du type réel de l'objet.
  • Pour personnaliser une méthode tout en réutilisant le code de la classe mère, appelez d'abord la méthode de la classe mère (ClasseMere::methode()).
  • Le masquage ne remplace pas le polymorphisme, qui nécessite des méthodes virtual.

Chapitre 4 : Le Polymorphisme

Objectifs d'Apprentissage 🎯

  • Comprendre la problématique résolue par le polymorphisme.
  • Savoir déclarer et utiliser des méthodes virtual.
  • Maîtriser l'impact du polymorphisme sur l'appel des méthodes.
  • Connaître les bonnes pratiques pour implémenter le polymorphisme.

4.1 Problématique du Polymorphisme

Le polymorphisme permet de stocker et manipuler des objets de types différents dans un même conteneur, tout en garantissant que la bonne méthode est appelée en fonction du type réel de l'objet.

Exemple de Problématique :

class Personne {
public:
    void afficher() { std::cout << "Personne\n"; }
};

class Etudiant : public Personne {
public:
    void afficher() { std::cout << "Etudiant\n"; }
};

int main() {
    std::vector<Personne*> personnes;
    personnes.push_back(new Personne());
    personnes.push_back(new Etudiant());
    
    for (Personne* p : personnes) {
        p->afficher(); // Affiche toujours "Personne" (pas de polymorphisme)
    }
    
    return 0;
}

Dans cet exemple, bien que le deuxième élément du vecteur soit un Etudiant, la méthode afficher() de Personne est appelée, car la méthode n'est pas virtual.

4.2 Solution : Les Méthodes virtual

Pour activer le polymorphisme, déclarez la méthode dans la classe mère avec le mot-clé virtual. Ainsi, l'appel de la méthode dépendra du type réel de l'objet, et non du type statique du pointeur ou de la référence.

Exemple avec Polymorphisme :

class Personne {
public:
    virtual void afficher() { std::cout << "Personne\n"; }
};

class Etudiant : public Personne {
public:
    void afficher() override { std::cout << "Etudiant\n"; }
};

int main() {
    std::vector<Personne*> personnes;
    personnes.push_back(new Personne());
    personnes.push_back(new Etudiant());
    
    for (Personne* p : personnes) {
        p->afficher(); // Affiche "Personne" puis "Etudiant" (polymorphisme activé)
    }
    
    return 0;
}

Notes de l'IA 🧑‍🏫 :

  • Le mot-clé override (C++11 et ultérieur) permet de vérifier à la compilation que la méthode redéfinie dans la classe fille est bien virtual dans la classe mère. Cela évite les erreurs de typographie ou de signature.
  • Une méthode virtual dans une classe mère reste virtual dans toutes les classes filles, même si le mot-clé virtual n'est pas répété.
  • Pour activer pleinement le polymorphisme, utilisez toujours des pointeurs ou des références (ex: Personne*, Personne&). Les objets eux-mêmes ne bénéficient pas du polymorphisme.

4.3 Destructeurs virtual

Si une classe est conçue pour être héritée, son destructeur doit être déclaré virtual. Cela garantit que le destructeur de la classe fille est appelé lorsque l'objet est détruit via un pointeur de la classe mère.

Exemple :

class Personne {
public:
    virtual ~Personne() {}
};

class Etudiant : public Personne {
public:
    ~Etudiant() override { std::cout << "Destruction Etudiant\n"; }
};

int main() {
    Personne* p = new Etudiant();
    delete p; // Appelle ~Etudiant() puis ~Personne() (grâce au destructeur virtual)
    return 0;
}

Notes de l'IA 🧑‍🏫 :

  • Oublier de déclarer un destructeur virtual dans une classe mère peut entraîner des fuites mémoire ou des comportements indéfinis, car seul le destructeur de la classe mère sera appelé.
  • Si une classe n'est pas conçue pour être héritée, son destructeur n'a pas besoin d'être virtual. Cependant, il est souvent recommandé de le déclarer virtual par précaution.

Points Clés à Retenir 📌

  • Le polymorphisme permet d'appeler la bonne méthode en fonction du type réel de l'objet (et non du type statique).
  • Pour activer le polymorphisme, déclarez la méthode dans la classe mère avec le mot-clé virtual.
  • Utilisez toujours des pointeurs ou des références pour bénéficier du polymorphisme.
  • Déclarez le destructeur virtual dans une classe mère pour garantir une destruction correcte des objets dérivés.
  • Le mot-clé override permet de vérifier que la méthode redéfinie est bien virtual dans la classe mère.

Chapitre 5 : Conteneurs et Itérateurs en C++

Objectifs d'Apprentissage 🎯

  • Comprendre l'utilité des itérateurs et leur rôle dans la STL.
  • Savoir parcourir un conteneur à l'aide d'itérateurs.
  • Connaître les principaux conteneurs de la STL et leurs caractéristiques.
  • Utiliser les algorithmes génériques de la bibliothèque <algorithm> avec des itérateurs.

5.1 Les Itérateurs

Les itérateurs sont des objets généralisés permettant de parcourir et d'interagir avec les éléments d'un conteneur, indépendamment de son type. Ils offrent plusieurs avantages :

  • Flexibilité : Compatibles avec tous les conteneurs de la STL.
  • Précision : Permettent un accès fin aux éléments (lecture, modification, insertion, suppression).
  • Généricité : Utilisables avec les algorithmes de la bibliothèque <algorithm>.

Exemple de Parcours avec Itérateur :

#include <vector>
#include <iostream>

int main() {
    std::vector<int> v = {10, 20, 30, 40};
    
    // Déclaration d'un itérateur
    std::vector<int>::iterator it;
    
    // Parcours du vecteur
    for (it = v.begin(); it != v.end(); ++it) {
        std::cout << *it << " "; // Affiche : 10 20 30 40
    }
    
    return 0;
}

Notes de l'IA 🧑‍🏫 :

  • Les itérateurs se comportent comme des pointeurs intelligents. L'opérateur * permet d'accéder à l'élément pointé, et l'opérateur ++ permet de passer à l'élément suivant.
  • v.begin() retourne un itérateur pointant sur le premier élément du conteneur.
  • v.end() retourne un itérateur pointant sur la position juste après le dernier élément (et non sur le dernier élément).
  • Pour les conteneurs constants, utilisez const_iterator pour éviter les modifications accidentelles.

5.2 Les Conteneurs de la STL

La STL propose plusieurs types de conteneurs, chacun adapté à des besoins spécifiques :

Conteneurs Séquentiels

ConteneurDescriptionAccèsInsertion/Suppression
vectorTableau dynamique. Taille modifiable, accès aléatoire rapide.O(1)O(n) (sauf en fin)
listListe doublement chaînée. Insertion/suppression rapide en milieu de liste.O(n)O(1)
dequeFile à double extrémité. Accès aléatoire et insertion/suppression rapides aux deux extrémités.O(1)O(1) (aux extrémités)

Conteneurs Associatifs

ConteneurDescriptionAccès
setCollection d'éléments uniques, triés automatiquement.O(log n)
mapCollection de paires clé/valeur, triées par clé.O(log n)
unordered_setCollection d'éléments uniques, non triés (basé sur une table de hachage).O(1) en moyenne
unordered_mapCollection de paires clé/valeur, non triées (basé sur une table de hachage).O(1) en moyenne

Adaptateurs de Conteneurs

AdaptateurDescriptionConteneur Sous-Jacent
stackPile (LIFO : Last In, First Out).deque (par défaut)
queueFile (FIFO : First In, First Out).deque (par défaut)
priority_queueFile de priorité. Les éléments sont triés par ordre de priorité.vector (par défaut)

5.3 La Bibliothèque <algorithm>

La bibliothèque <algorithm> fournit des algorithmes génériques qui opèrent sur des plages d'éléments définies par des itérateurs. Voici quelques fonctions couramment utilisées :

FonctionDescriptionExemple d'Utilisation
sortTrie les éléments d'une plage.sort(v.begin(), v.end());
findRecherche un élément dans une plage.auto it = find(v.begin(), v.end(), 42);
countCompte le nombre d'occurrences d'une valeur dans une plage.int n = count(v.begin(), v.end(), 42);
copyCopie les éléments d'une plage vers une autre.copy(v1.begin(), v1.end(), v2.begin());
reverseInverse l'ordre des éléments dans une plage.reverse(v.begin(), v.end());

Exemple d'Utilisation :

#include <vector>
#include <algorithm>
#include <iostream>

int main() {
    std::vector<int> v = {40, 10, 30, 20};
    
    // Tri du vecteur
    sort(v.begin(), v.end());
    
    // Recherche d'un élément
    auto it = find(v.begin(), v.end(), 30);
    if (it != v.end()) {
        std::cout << "Element trouvé : " << *it << "\n";
    }
    
    return 0;
}

Points Clés à Retenir 📌

  • Les itérateurs permettent de parcourir et manipuler les éléments d'un conteneur de manière générique.
  • La STL propose une variété de conteneurs adaptés à différents besoins (accès rapide, insertion/suppression efficace, tri automatique, etc.).
  • Les algorithmes génériques de la bibliothèque <algorithm> fonctionnent avec tous les conteneurs via les itérateurs.
  • Choisissez le conteneur en fonction des opérations les plus fréquentes dans votre programme (ex: vector pour un accès aléatoire fréquent, list pour des insertions/suppressions fréquentes en milieu de conteneur).
  • Les adaptateurs de conteneurs (stack, queue, priority_queue) fournissent des interfaces spécialisées pour des structures de données courantes.