Cours sur 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
Chienest unAnimal).
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
privated'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é, utilisezprotected.- Le mot-clé
protectedest 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éritage | Effet sur les Membres de la Classe Mère |
|---|---|
public | Les membres public/protected restent inchangés. |
protected | Les membres public/protected deviennent protected. |
private | Les 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
privated'une classe mère sont toujours inaccessibles à la classe fille. - L'héritage
publicest 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 :
- Constructeur de la classe mère (de la plus générale à la plus spécifique).
- 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 :
- Appeler la méthode de la classe mère depuis la méthode masquée de la classe fille.
- 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 bienvirtualdans la classe mère. Cela évite les erreurs de typographie ou de signature.- Une méthode
virtualdans une classe mère restevirtualdans toutes les classes filles, même si le mot-clévirtualn'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
virtualdans 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éclarervirtualpar 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
virtualdans une classe mère pour garantir une destruction correcte des objets dérivés. - Le mot-clé
overridepermet de vérifier que la méthode redéfinie est bienvirtualdans 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_iteratorpour é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
| Conteneur | Description | Accès | Insertion/Suppression |
|---|---|---|---|
vector | Tableau dynamique. Taille modifiable, accès aléatoire rapide. | O(1) | O(n) (sauf en fin) |
list | Liste doublement chaînée. Insertion/suppression rapide en milieu de liste. | O(n) | O(1) |
deque | File à 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
| Conteneur | Description | Accès |
|---|---|---|
set | Collection d'éléments uniques, triés automatiquement. | O(log n) |
map | Collection de paires clé/valeur, triées par clé. | O(log n) |
unordered_set | Collection d'éléments uniques, non triés (basé sur une table de hachage). | O(1) en moyenne |
unordered_map | Collection de paires clé/valeur, non triées (basé sur une table de hachage). | O(1) en moyenne |
Adaptateurs de Conteneurs
| Adaptateur | Description | Conteneur Sous-Jacent |
|---|---|---|
stack | Pile (LIFO : Last In, First Out). | deque (par défaut) |
queue | File (FIFO : First In, First Out). | deque (par défaut) |
priority_queue | File 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 :
| Fonction | Description | Exemple d'Utilisation |
|---|---|---|
sort | Trie les éléments d'une plage. | sort(v.begin(), v.end()); |
find | Recherche un élément dans une plage. | auto it = find(v.begin(), v.end(), 42); |
count | Compte le nombre d'occurrences d'une valeur dans une plage. | int n = count(v.begin(), v.end(), 42); |
copy | Copie les éléments d'une plage vers une autre. | copy(v1.begin(), v1.end(), v2.begin()); |
reverse | Inverse 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:
vectorpour un accès aléatoire fréquent,listpour 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.