Module 04 : Polymorphisme, classes abstraites et interfaces
Concepts clés :
- Fonctions virtuelles
- Polymorphisme à l’exécution
- Destructeurs virtuels
- Classes abstraites (virtuelles pures)
- Interfaces
- Copie profonde avec polymorphisme
Pourquoi c’est important
Section intitulée « Pourquoi c’est important »Le polymorphisme est l’une des fonctionnalités les plus puissantes de la programmation orientée objet. Il vous permet d’écrire du code flexible et extensible.
La fondation : La relation EST-UN permet le polymorphisme
Section intitulée « La fondation : La relation EST-UN permet le polymorphisme »Le polymorphisme s’appuie sur la relation EST-UN de l’héritage :
class Animal { };class Dog : public Animal { }; // Un Dog EST-UN Animalclass Cat : public Animal { }; // Un Cat EST-UN AnimalParce qu’un Dog EST-UN Animal, vous pouvez traiter les Dogs comme des Animals :
Animal* pet = new Dog(); // Valide ! Un Dog EST-UN AnimalC’est la fondation du polymorphisme : vous pouvez utiliser des objets dérivés partout où des objets de base sont attendus. La magie des fonctions virtuelles est qu’appeler pet->makeSound() exécute Dog::makeSound() même si pet est déclaré comme Animal*.
Comprendre les opérateurs du polymorphisme
Section intitulée « Comprendre les opérateurs du polymorphisme »Ce module introduit les fonctions virtuelles et les classes abstraites. Voici la syntaxe expliquée :
Le mot-clé virtual (Modificateur de fonction)
Section intitulée « Le mot-clé virtual (Modificateur de fonction) »virtual void makeSound() { /* ... */ }virtualavant une déclaration de fonction active le polymorphisme à l’exécution- Sans
virtual, C++ utilise le type statique (type à la compilation) pour décider quelle fonction appeler - Avec
virtual, C++ utilise le type dynamique (type réel de l’objet à l’exécution) pour décider - Cela permet à un
Animal*d’appelerDog::makeSound()s’il pointe réellement vers un Dog
La syntaxe = 0 (Virtuelle pure)
Section intitulée « La syntaxe = 0 (Virtuelle pure) »virtual void makeSound() = 0; // Fonction virtuelle pure= 0rend une fonction virtuelle « virtuelle pure » (abstraite)- Cela signifie que la classe ne fournit AUCUNE implémentation - les classes dérivées DOIVENT la redéfinir
- Toute classe avec au moins une fonction virtuelle pure devient une classe abstraite
- Vous NE POUVEZ PAS créer d’objets de classes abstraites :
Animal pet;est une erreur
L’opérateur * (Pointeurs polymorphiques)
Section intitulée « L’opérateur * (Pointeurs polymorphiques) »Animal* pet = new Dog(); // Pointeur vers base, objet est dérivépet->makeSound(); // Appelle Dog::makeSound() (si virtual)*avec polymorphisme : déclarer des pointeurs vers la classe de base, mais pointer vers des objets dérivés->accède aux membres via le pointeurvirtualassure que la bonne fonction (dérivée) est appelée- Sans
virtual, vous obtiendriez toujours la version de la classe de base
L’opérateur & (Références polymorphiques)
Section intitulée « L’opérateur & (Références polymorphiques) »void makeNoise(Animal& animal) { // Peut passer Dog, Cat, etc. animal.makeSound(); // Le bon son est joué}&crée une référence vers la classe de base- Comme les pointeurs, les références permettent le polymorphisme
- Le type réel de l’objet détermine quelle fonction s’exécute
- Les références sont plus sûres que les pointeurs (ne peuvent pas être null, ne peuvent pas être réassignées)
L’opérateur delete avec polymorphisme
Section intitulée « L’opérateur delete avec polymorphisme »Animal* pet = new Dog();delete pet; // Sans destructeur virtuel, le destructeur de Dog ne s'exécute jamais !deletesur un pointeur de classe de base n’appelle que le destructeur de base (par défaut)- Cela cause des fuites de mémoire si la classe dérivée a alloué des ressources
- Solution : Rendre le destructeur de la classe de base
virtual - Avec
virtual ~Animal(),delete petappelle~Dog()puis~Animal()- correct !
L’opérateur * (Type de retour du patron Clone)
Section intitulée « L’opérateur * (Type de retour du patron Clone) »virtual Animal* clone() const = 0; // Chaque classe dérivée retourne son propre type*dans les types de retour indique qu’un pointeur est retourné- Le patron Clone retourne une nouvelle copie de l’objet
- Chaque classe dérivée redéfinit pour retourner son propre type :
Dog*,Cat*, etc. - Cela permet de copier via des pointeurs de classe de base tout en préservant le type réel
L’opérateur () (Cast)
Section intitulée « L’opérateur () (Cast) »dynamic_cast<Dog*>(animal); // Cast sécurisé à l'exécutiondynamic_cast<>convertit de manière sécurisée les pointeurs de classe de base vers des pointeurs de classe dérivée- Il vérifie à l’exécution si le cast est valide
- Retourne
NULL(pour les pointeurs) ou lance une exception (pour les références) si le cast échoue - Fonctionne seulement avec les classes polymorphiques (classes avec fonctions virtuelles)
1. Le problème sans polymorphisme
Section intitulée « 1. Le problème sans polymorphisme »class Animal {public: void makeSound() { std::cout << "Some sound" << std::endl; }};
class Dog : public Animal {public: void makeSound() { std::cout << "Woof!" << std::endl; }};
class Cat : public Animal {public: void makeSound() { std::cout << "Meow!" << std::endl; }};
// LE PROBLÈME :Animal* pet = new Dog();pet->makeSound(); // Affiche "Some sound" - PAS "Woof!"
// Pourquoi ? Sans virtual, C++ utilise la liaison STATIQUE// Il voit Animal* et appelle Animal::makeSound()2. Fonctions virtuelles
Section intitulée « 2. Fonctions virtuelles »La solution
Section intitulée « La solution »class Animal {public: virtual void makeSound() { std::cout << "Some sound" << std::endl; }};
class Dog : public Animal {public: void makeSound() { // Redéfinit la fonction virtuelle std::cout << "Woof!" << std::endl; }};
// MAINTENANT :Animal* pet = new Dog();pet->makeSound(); // Affiche "Woof!" - correct !
// Pourquoi ? Avec virtual, C++ utilise la liaison DYNAMIQUE// Il vérifie le type réel de l'objet à l'exécutionComment fonctionne virtual
Section intitulée « Comment fonctionne virtual »// Vue simplifiée de ce qui se passe :
// Sans virtual (liaison statique) :// Le compilateur voit : Animal* ptr// Le compilateur appelle : Animal::makeSound() - décidé à la compilation
// Avec virtual (liaison dynamique) :// L'objet contient un pointeur caché vers "vtable" (table virtuelle)// vtable contient des pointeurs vers les bonnes fonctions pour cette classe// À l'exécution, la bonne fonction est recherchée et appeléeVisualisation de la table virtuelle
Section intitulée « Visualisation de la table virtuelle »┌─────────────────────────────────────────────────────────────────┐│ Comment fonctionnent les fonctions virtuelles (vtable) │├─────────────────────────────────────────────────────────────────┤│ ││ Disposition mémoire avec vptr et vtable : ││ ┌─────────────────────────────────────────────────────────┐ ││ │ │ ││ │ ┌────────────────┐ ┌────────────────┐ │ ││ │ │ Objet Animal │ │ Objet Dog │ │ ││ │ ├────────────────┤ ├────────────────┤ │ ││ │ │ [vptr]─────┐ │ │ [vptr]─────┐ │ │ ││ │ │ _type │ │ │ _type │ │ │ ││ │ │ _brain │ │ │ _brain │ │ │ ││ │ │ [data...] │ │ │ [data...] │ │ │ ││ │ └────────────┼───┘ └────────────┼───┘ │ ││ │ │ │ │ ││ │ └───────────┬───────────┘ │ ││ │ │ │ ││ │ ┌───────────▼───────────┐ │ ││ │ │ vptr (pointeur) │ │ ││ │ │ (8 octets en x64) │ │ ││ │ └───────────┬───────────┘ │ ││ │ │ │ ││ │ ┌───────────────────┴───────────────────┐ │ ││ │ ▼ ▼ │ ││ │ ┌───────────────────┐ ┌───────────────────┐ │ ││ │ │ vtable Animal │ │ vtable Dog │ │ ││ │ ├───────────────────┤ ├───────────────────┤ │ ││ │ │ [0] destructor ──┼──┐ │ [0] destructor ──┼──┐│ ││ │ │ [1] makeSound ──┼──┼──┐ │ [1] makeSound ──┼──┼┘│ ││ │ │ [2] clone ──┼──┼──┼──│ [2] clone ──┼──┼─┘ ││ │ │ [3] ... ──┼──┼──┼──│ [3] ... ──┼──┘ ││ │ └───────────────────┘ │ │ └───────────────────┘ ││ │ │ │ ││ │ ▼ ▼ ││ │ ┌─────────────────┐ ││ │ │ Pointeurs de │ ││ │ │ fonctions │ ││ │ │ │ ││ │ │ ┌─────────────┐ │ ││ │ │ │Animal:: │ │ ││ │ │ │makeSound() │ │ ││ │ │ └─────────────┘ │ ││ │ │ ┌─────────────┐ │ ││ │ │ │Dog:: │ │ ││ │ │ │makeSound() │ │ ││ │ │ └─────────────┘ │ ││ │ │ ┌─────────────┐ │ ││ │ │ │Dog:: │ │ ││ │ │ │clone() │ │ ││ │ │ └─────────────┘ │ ││ │ └─────────────────┘ ││ │ ││ └─────────────────────────────────────────────────────────┘ ││ ││ Processus de dispatch dynamique : ││ ┌────────────────────────────────────────────────────────┐ ││ │ │ ││ │ Animal* pet = new Dog(); │ ││ │ │ │ ││ │ │ pet pointe vers objet Dog (a vtable Dog) │ ││ │ ▼ │ ││ │ pet->makeSound(); // Recherche à l'exécution │ ││ │ │ │ ││ │ ▼ 1. Suivre vptr dans l'objet │ ││ │ ┌─────────────────┐ │ ││ │ │ vptr → Dog │ │ ││ │ │ vtable │ │ ││ │ └────────┬────────┘ │ ││ │ ▼ 2. Chercher index makeSound │ ││ │ ┌─────────────────┐ │ ││ │ │ slot makeSound │ → Dog::makeSound() │ ││ │ │ (index 1) │ "Woof!" │ ││ │ └─────────────────┘ │ ││ │ │ │ ││ │ ▼ 3. Appeler la fonction │ ││ │ Résultat : "Woof!" (pas "Some sound") │ ││ │ │ ││ └────────────────────────────────────────────────────────┘ ││ ││ Points clés : ││ • Chaque objet avec fonctions virtuelles a un vptr caché ││ • vptr pointe vers la vtable de la classe (statique, partagée) ││ • vtable contient pointeurs vers implémentations redéfinies ││ • Coût exécution : une indirection de pointeur supplémentaire ││ • Coût mémoire : un pointeur supplémentaire par objet ││ │└─────────────────────────────────────────────────────────────────┘3. Destructeurs virtuels (CRITIQUE !)
Section intitulée « 3. Destructeurs virtuels (CRITIQUE !) »Le problème
Section intitulée « Le problème »class Animal {public: ~Animal() { std::cout << "Animal destroyed" << std::endl; }};
class Dog : public Animal {private: Brain* _brain;public: Dog() { _brain = new Brain(); } ~Dog() { delete _brain; std::cout << "Dog destroyed" << std::endl; }};
Animal* pet = new Dog();delete pet; // Appelle SEULEMENT ~Animal() ! // Le brain de Dog est FUIT !La solution
Section intitulée « La solution »class Animal {public: virtual ~Animal() { std::cout << "Animal destroyed" << std::endl; } //^^^^^^^ TOUJOURS rendre les destructeurs virtuels dans les classes de base};
Animal* pet = new Dog();delete pet; // Appelle ~Dog() PUIS ~Animal() - correct !Si une classe a N’IMPORTE QUELLE fonction virtuelle, rendez son destructeur virtuel.
4. Classes abstraites
Section intitulée « 4. Classes abstraites »Qu’est-ce qu’une classe abstraite ?
Section intitulée « Qu’est-ce qu’une classe abstraite ? »Une classe qui NE PEUT PAS être instanciée. Elle sert de modèle (ou contrat) pour les classes dérivées.
Le concept de contrat : Une classe abstraite définit CE QUE les classes dérivées doivent faire sans spécifier COMMENT. Quand vous écrivez virtual void makeSound() = 0;, vous dites : « Toute classe qui hérite de moi DOIT fournir une implémentation de makeSound(). » C’est un contrat que les classes dérivées doivent respecter.
Pourquoi les contrats sont importants :
- Garantit le comportement : Le code utilisant
Animal*peut appelermakeSound()en sachant qu’il existe - Impose la complétude : Le compilateur ne vous laissera pas instancier une classe qui ne remplit pas le contrat
- Permet le polymorphisme : Vous pouvez écrire du code générique qui fonctionne avec n’importe quel Animal
Fonctions virtuelles pures
Section intitulée « Fonctions virtuelles pures »class Animal {public: // Fonction virtuelle pure - pas d'implémentation virtual void makeSound() const = 0; // ^^^ La rend virtuelle pure
virtual ~Animal() {}};
// Maintenant Animal est abstraite :Animal pet; // ERREUR : impossible d'instancier une classe abstraiteAnimal* ptr; // OK : peut avoir un pointeur vers une classe abstraiteptr = new Dog(); // OK : peut pointer vers une classe dérivée concrèteClasses dérivées concrètes
Section intitulée « Classes dérivées concrètes »class Dog : public Animal {public: // DOIT implémenter TOUTES les fonctions virtuelles pures void makeSound() const { std::cout << "Woof!" << std::endl; }};
Dog dog; // OK : Dog est concrète (implémente toutes les virtuelles pures)Classes partiellement abstraites
Section intitulée « Classes partiellement abstraites »class Animal {public: virtual void makeSound() const = 0; // Virtuelle pure virtual void eat() { /* défaut */ } // Virtuelle avec défaut};
class Dog : public Animal {public: void makeSound() const { /* ... */ } // Doit implémenter // eat() héritée avec implémentation par défaut};5. Interfaces
Section intitulée « 5. Interfaces »Qu’est-ce qu’une interface ?
Section intitulée « Qu’est-ce qu’une interface ? »Une classe avec UNIQUEMENT des fonctions virtuelles pures. Définit un contrat sans aucune implémentation.
// Convention de nommage des interfaces : préfixe avec 'I'class ICharacter {public: virtual ~ICharacter() {} virtual std::string const& getName() const = 0; virtual void equip(AMateria* m) = 0; virtual void unequip(int idx) = 0; virtual void use(int idx, ICharacter& target) = 0;};Implémenter une interface
Section intitulée « Implémenter une interface »class Character : public ICharacter {private: std::string _name; AMateria* _inventory[4];
public: Character(std::string name); Character(const Character& other); Character& operator=(const Character& other); ~Character();
// Doit implémenter TOUTES les méthodes de l'interface std::string const& getName() const; void equip(AMateria* m); void unequip(int idx); void use(int idx, ICharacter& target);};6. Copie profonde avec polymorphisme
Section intitulée « 6. Copie profonde avec polymorphisme »Le problème
Section intitulée « Le problème »class Animal {protected: Brain* _brain;public: Animal() { _brain = new Brain(); } Animal(const Animal& other) { _brain = new Brain(*other._brain); } virtual ~Animal() { delete _brain; }};
class Dog : public Animal { /* ... */ };
// Problème : copier via un pointeur de baseAnimal* original = new Dog();Animal* copy = new Animal(*original); // Crée un Animal, pas un Dog !Le patron Clone
Section intitulée « Le patron Clone »class Animal {public: virtual Animal* clone() const = 0; virtual ~Animal() {}};
class Dog : public Animal {public: Dog* clone() const { return new Dog(*this); }};
Animal* original = new Dog();Animal* copy = original->clone(); // Crée un Dog !Patron Clone : Implémentation complète
Section intitulée « Patron Clone : Implémentation complète »class AMateria {protected: std::string _type;public: AMateria(std::string const& type) : _type(type) {} virtual ~AMateria() {}
std::string const& getType() const { return _type; } virtual AMateria* clone() const = 0; // Virtuelle pure};
class Ice : public AMateria {public: Ice() : AMateria("ice") {} Ice(const Ice& other) : AMateria(other) {}
// Type de retour covariant - retourne Ice* au lieu de AMateria* Ice* clone() const { return new Ice(*this); // Utilise le constructeur de copie }};
class Cure : public AMateria {public: Cure() : AMateria("cure") {} Cure(const Cure& other) : AMateria(other) {}
Cure* clone() const { return new Cure(*this); }};Gestion de la mémoire avec Clone
Section intitulée « Gestion de la mémoire avec Clone »class Character {private: AMateria* _inventory[4];public: // Copie profonde utilisant clone Character(const Character& other) { for (int i = 0; i < 4; i++) { if (other._inventory[i]) _inventory[i] = other._inventory[i]->clone(); else _inventory[i] = NULL; } }
// Assignation utilisant clone Character& operator=(const Character& other) { if (this != &other) { // Supprimer l'ancien inventaire for (int i = 0; i < 4; i++) delete _inventory[i]; // Cloner le nouvel inventaire for (int i = 0; i < 4; i++) { if (other._inventory[i]) _inventory[i] = other._inventory[i]->clone(); else _inventory[i] = NULL; } } return *this; }
~Character() { for (int i = 0; i < 4; i++) delete _inventory[i]; }};7. Tableaux de pointeurs de classe de base
Section intitulée « 7. Tableaux de pointeurs de classe de base »Créer et gérer des tableaux polymorphiques
Section intitulée « Créer et gérer des tableaux polymorphiques »// Tableau de pointeurs Animal (peut contenir Dogs, Cats, etc.)Animal* animals[4];
animals[0] = new Dog();animals[1] = new Cat();animals[2] = new Dog();animals[3] = new Cat();
// Comportement polymorphiquefor (int i = 0; i < 4; i++) { animals[i]->makeSound(); // Appelle la bonne version}
// CRITIQUE : Doit supprimer chaque élémentfor (int i = 0; i < 4; i++) { delete animals[i]; // Le destructeur virtuel assure le bon nettoyage}Pourquoi le destructeur virtuel est critique
Section intitulée « Pourquoi le destructeur virtuel est critique »// SANS destructeur virtuel :Animal* pet = new Dog(); // Dog alloue Braindelete pet; // Seul ~Animal() appelé - Brain fuit !
// AVEC destructeur virtuel :Animal* pet = new Dog(); // Dog alloue Braindelete pet; // ~Dog() appelé en premier (supprime Brain), puis ~Animal()8. Patron Factory (MateriaSource)
Section intitulée « 8. Patron Factory (MateriaSource) »Le patron Factory crée des objets sans exposer la logique d’instanciation.
class IMateriaSource {public: virtual ~IMateriaSource() {} virtual void learnMateria(AMateria*) = 0; virtual AMateria* createMateria(std::string const& type) = 0;};
class MateriaSource : public IMateriaSource {private: AMateria* _templates[4];
public: MateriaSource() { for (int i = 0; i < 4; i++) _templates[i] = NULL; }
// Copie profonde dans le constructeur de copie MateriaSource(const MateriaSource& other) { for (int i = 0; i < 4; i++) { if (other._templates[i]) _templates[i] = other._templates[i]->clone(); else _templates[i] = NULL; } }
~MateriaSource() { for (int i = 0; i < 4; i++) delete _templates[i]; }
void learnMateria(AMateria* m) { if (!m) return; for (int i = 0; i < 4; i++) { if (_templates[i] == NULL) { _templates[i] = m->clone(); // Stocker une COPIE return; } } }
// Méthode factory - crée de nouveaux objets basés sur une chaîne type AMateria* createMateria(std::string const& type) { for (int i = 0; i < 4; i++) { if (_templates[i] && _templates[i]->getType() == type) return _templates[i]->clone(); // Retourner une NOUVELLE copie } return NULL; }};Utilisation de la Factory
Section intitulée « Utilisation de la Factory »IMateriaSource* src = new MateriaSource();
// Enseigner à la factory ce qu'elle peut créersrc->learnMateria(new Ice());src->learnMateria(new Cure());
// La factory crée de nouvelles instancesAMateria* ice = src->createMateria("ice"); // Nouvel objet IceAMateria* cure = src->createMateria("cure"); // Nouvel objet CureAMateria* unknown = src->createMateria("fire"); // NULL - pas appris
delete src;Points clés du patron Factory
Section intitulée « Points clés du patron Factory »- Découple la création de l’utilisation : Le client n’a pas besoin de connaître les types concrets
- Utilise clone() : Les nouveaux objets sont des copies des templates
- Propriété de la mémoire : La Factory possède les templates, l’appelant possède les objets créés
Exercice 00 : Polymorphisme
Section intitulée « Exercice 00 : Polymorphisme »Analyse du sujet
Section intitulée « Analyse du sujet »Créez une classe de base Animal avec des classes dérivées Dog et Cat :
- Animal a un attribut
typeet une méthodemakeSound() - Dog dit “Woof!”, Cat dit “Meow!”
- La clé : Quand vous utilisez un pointeur
Animal*vers unDog, appelermakeSound()devrait afficher “Woof!”, pas un son générique
Créez aussi des versions “Wrong” pour démontrer ce qui se passe sans polymorphisme.
Stratégie d’approche
Section intitulée « Stratégie d’approche »Étape 1 - Comprendre le mot-clé virtual
Sans virtual :
Animal* pet = new Dog();pet->makeSound(); // Appelle Animal::makeSound() - FAUX !Avec virtual :
Animal* pet = new Dog();pet->makeSound(); // Appelle Dog::makeSound() - CORRECT !Étape 2 - Le destructeur virtuel est CRITIQUE
Lors de la suppression via un pointeur de base, vous avez besoin d’un destructeur virtuel pour appeler le destructeur dérivé.
Étape 3 - Créer WrongAnimal/WrongCat pour comparaison
Mêmes classes sans virtual pour démontrer la différence.
Construction progressive du code
Section intitulée « Construction progressive du code »Étape 1 - Classe de base avec fonctions virtuelles :
#ifndef ANIMAL_HPP#define ANIMAL_HPP
#include <string>
class Animal {protected: std::string _type;
public: Animal(); Animal(const Animal& other); Animal& operator=(const Animal& other); virtual ~Animal(); // Destructeur VIRTUEL !
virtual void makeSound() const; // VIRTUEL pour le polymorphisme std::string getType() const;};
#endifÉtape 2 - Classe dérivée redéfinissant :
#ifndef DOG_HPP#define DOG_HPP
#include "Animal.hpp"
class Dog : public Animal {public: Dog(); Dog(const Dog& other); Dog& operator=(const Dog& other); virtual ~Dog();
void makeSound() const; // Override};
#endif#include "Dog.hpp"#include <iostream>
Dog::Dog() : Animal() { _type = "Dog"; std::cout << "Dog constructed" << std::endl;}
Dog::~Dog() { std::cout << "Dog destructed" << std::endl;}
void Dog::makeSound() const { std::cout << "Woof!" << std::endl;}Étape 3 - Versions Wrong (sans virtual) :
class WrongAnimal {protected: std::string _type;
public: WrongAnimal(); ~WrongAnimal(); // PAS virtuel !
void makeSound() const; // PAS virtuel ! std::string getType() const;};Explication ligne par ligne
Section intitulée « Explication ligne par ligne »| Ligne | Code | Pourquoi |
|---|---|---|
virtual ~Animal() | Destructeur virtuel | Assure que le destructeur dérivé est appelé via pointeur base |
virtual void makeSound() const | Fonction virtuelle | Active le polymorphisme à l’exécution |
void makeSound() const (in Dog) | Override | Fournit l’implémentation spécifique à Dog |
_type = "Dog" | Définir type dérivé | Identifier le type réel |
Pièges courants
Section intitulée « Pièges courants »1. Oublier le mot-clé virtual
// FAUX - pas de polymorphismeclass Animal {public: void makeSound() const; // Pas virtuel};
Animal* pet = new Dog();pet->makeSound(); // Appelle Animal::makeSound(), PAS Dog !
// CORRECT - polymorphiqueclass Animal {public: virtual void makeSound() const;};2. Un destructeur non-virtuel cause des fuites mémoire
// FAUXclass Animal {public: ~Animal(); // PAS virtuel};
Animal* pet = new Dog();delete pet; // Seul ~Animal() appelé ! ~Dog() sauté !
// CORRECTclass Animal {public: virtual ~Animal(); // Destructeur virtuel};Conseils de test
Section intitulée « Conseils de test »int main() { // Tester le polymorphisme const Animal* animal = new Animal(); const Animal* dog = new Dog(); const Animal* cat = new Cat();
std::cout << animal->getType() << ": "; animal->makeSound(); // "Generic animal sound" ou similaire
std::cout << dog->getType() << ": "; dog->makeSound(); // "Woof!"
std::cout << cat->getType() << ": "; cat->makeSound(); // "Meow!"
delete animal; delete dog; delete cat;
// Comparer avec WrongAnimal std::cout << "\n--- Wrong versions ---\n"; const WrongAnimal* wrongCat = new WrongCat(); wrongCat->makeSound(); // Appelle WrongAnimal::makeSound(), PAS WrongCat ! delete wrongCat;
return 0;}Code final
Section intitulée « Code final »#include "Animal.hpp"#include <iostream>
Animal::Animal() : _type("Animal") { std::cout << "Animal constructed" << std::endl;}
Animal::Animal(const Animal& other) : _type(other._type) { std::cout << "Animal copy constructed" << std::endl;}
Animal& Animal::operator=(const Animal& other) { if (this != &other) _type = other._type; return *this;}
Animal::~Animal() { std::cout << "Animal destructed" << std::endl;}
void Animal::makeSound() const { std::cout << "* Generic animal sound *" << std::endl;}
std::string Animal::getType() const { return _type;}Exercice 01 : I Don’t Want to Set the World on Fire
Section intitulée « Exercice 01 : I Don’t Want to Set the World on Fire »Analyse du sujet
Section intitulée « Analyse du sujet »Ajoutez une classe Brain à Dog et Cat :
- Brain a un tableau de 100 chaînes (ideas)
- Dog et Cat ont chacun un membre
Brain* - Critique : Vous devez implémenter la copie profonde - chaque Dog/Cat a son propre Brain
Cela teste la bonne gestion de la mémoire avec le polymorphisme.
Stratégie d’approche
Section intitulée « Stratégie d’approche »Étape 1 - Créer la classe Brain
Classe simple avec un tableau de chaînes.
Étape 2 - Ajouter un pointeur Brain à Dog/Cat
Brain* _brain - alloué dans le constructeur, supprimé dans le destructeur.
Étape 3 - Implémenter la copie profonde
Le constructeur de copie et l’opérateur d’assignation doivent créer une NOUVELLE copie de Brain, pas partager le pointeur.
Construction progressive du code
Section intitulée « Construction progressive du code »Étape 1 - Classe Brain :
#ifndef BRAIN_HPP#define BRAIN_HPP
#include <string>
class Brain {public: std::string ideas[100];
Brain(); Brain(const Brain& other); Brain& operator=(const Brain& other); ~Brain();};
#endif#include "Brain.hpp"#include <iostream>
Brain::Brain() { std::cout << "Brain constructed" << std::endl;}
Brain::Brain(const Brain& other) { for (int i = 0; i < 100; i++) ideas[i] = other.ideas[i]; std::cout << "Brain copy constructed" << std::endl;}
Brain& Brain::operator=(const Brain& other) { if (this != &other) { for (int i = 0; i < 100; i++) ideas[i] = other.ideas[i]; } return *this;}
Brain::~Brain() { std::cout << "Brain destructed" << std::endl;}Étape 2 - Dog avec Brain (copie profonde) :
class Dog : public Animal {private: Brain* _brain;
public: Dog(); Dog(const Dog& other); Dog& operator=(const Dog& other); ~Dog();
void makeSound() const; Brain* getBrain() const;};Dog::Dog() : Animal() { _type = "Dog"; _brain = new Brain(); // Allouer Brain std::cout << "Dog constructed" << std::endl;}
// Copie profonde - créer NOUVEAU BrainDog::Dog(const Dog& other) : Animal(other) { _brain = new Brain(*other._brain); // Copier le contenu de Brain std::cout << "Dog copy constructed" << std::endl;}
// Assignation avec copie profondeDog& Dog::operator=(const Dog& other) { if (this != &other) { Animal::operator=(other); delete _brain; // Libérer ancien Brain _brain = new Brain(*other._brain); // Copier nouveau Brain } return *this;}
Dog::~Dog() { delete _brain; // Libérer Brain std::cout << "Dog destructed" << std::endl;}Explication ligne par ligne
Section intitulée « Explication ligne par ligne »| Ligne | Code | Pourquoi |
|---|---|---|
Brain* _brain | Pointeur vers Brain | Allocation dynamique nécessaire |
_brain = new Brain() | Allouer dans constructeur | Chaque Dog a son propre Brain |
_brain = new Brain(*other._brain) | Copie profonde | Créer NOUVEAU Brain avec même contenu |
delete _brain | Dans assignation | Libérer ANCIEN Brain avant nouvelle allocation |
delete _brain | Dans destructeur | Nettoyer la mémoire |
Pièges courants
Section intitulée « Pièges courants »1. Copie superficielle (le désastre par défaut)
// FAUX - la copie par défaut partage le pointeur !Dog::Dog(const Dog& other) : Animal(other), _brain(other._brain) { // Les deux dogs pointent vers le MÊME Brain ! // Quand l'un est détruit, l'autre a un pointeur invalide !}
// CORRECT - copie profonde crée nouveau BrainDog::Dog(const Dog& other) : Animal(other) { _brain = new Brain(*other._brain); // Chaque dog a son propre Brain}2. Oublier le destructeur virtuel (fuite de Brain)
Animal* pet = new Dog(); // Dog alloue Braindelete pet; // Si ~Animal pas virtuel, ~Dog pas appelé = Brain fuit !3. Ne pas supprimer l’ancien Brain dans l’assignation
// FAUX - fuite mémoire !Dog& Dog::operator=(const Dog& other) { _brain = new Brain(*other._brain); // Ancien Brain fuit ! return *this;}
// CORRECTDog& Dog::operator=(const Dog& other) { delete _brain; // Libérer ancien _brain = new Brain(*other._brain); return *this;}Conseils de test
Section intitulée « Conseils de test »int main() { // Tester copie profonde Dog original; original.getBrain()->ideas[0] = "I love bones!";
Dog copy = original; // Constructeur de copie
// Modifier l'original original.getBrain()->ideas[0] = "I hate cats!";
// La copie ne devrait PAS être affectée (copie profonde a fonctionné) std::cout << "Original idea: " << original.getBrain()->ideas[0] << std::endl; std::cout << "Copy idea: " << copy.getBrain()->ideas[0] << std::endl; // Devrait afficher des chaînes différentes !
// Tester avec un tableau d'Animals Animal* animals[4]; animals[0] = new Dog(); animals[1] = new Dog(); animals[2] = new Cat(); animals[3] = new Cat();
for (int i = 0; i < 4; i++) delete animals[i]; // Devrait appeler les bons destructeurs
return 0;}Code final
Section intitulée « Code final »#include "Dog.hpp"#include <iostream>
Dog::Dog() : Animal() { _type = "Dog"; _brain = new Brain(); std::cout << "Dog constructed" << std::endl;}
Dog::Dog(const Dog& other) : Animal(other) { _brain = new Brain(*other._brain); std::cout << "Dog copy constructed" << std::endl;}
Dog& Dog::operator=(const Dog& other) { std::cout << "Dog assignment operator" << std::endl; if (this != &other) { Animal::operator=(other); delete _brain; _brain = new Brain(*other._brain); } return *this;}
Dog::~Dog() { delete _brain; std::cout << "Dog destructed" << std::endl;}
void Dog::makeSound() const { std::cout << "Woof!" << std::endl;}
Brain* Dog::getBrain() const { return _brain;}Exercice 02 : Classe abstraite
Section intitulée « Exercice 02 : Classe abstraite »Analyse du sujet
Section intitulée « Analyse du sujet »Rendez Animal abstraite - elle ne devrait pas être instanciable directement.
- Renommez
AnimalenAAnimal(le préfixeAindique abstraite) - Rendez
makeSound()une fonction virtuelle pure AAnimal a;devrait maintenant être une erreur de compilation
Stratégie d’approche
Section intitulée « Stratégie d’approche »Étape 1 - Ajouter = 0 pour rendre virtuelle pure
virtual void makeSound() const = 0; // Virtuelle pureÉtape 2 - Comprendre ce que signifie “abstraite”
- Impossible de créer des instances de classe abstraite
- PEUT avoir des pointeurs/références vers une classe abstraite
- Les classes dérivées DOIVENT implémenter les fonctions virtuelles pures
Construction progressive du code
Section intitulée « Construction progressive du code »#ifndef AANIMAL_HPP#define AANIMAL_HPP
#include <string>
class AAnimal {protected: std::string _type;
public: AAnimal(); AAnimal(const AAnimal& other); AAnimal& operator=(const AAnimal& other); virtual ~AAnimal();
virtual void makeSound() const = 0; // = 0 la rend virtuelle pure ! std::string getType() const;};
#endifExplication ligne par ligne
Section intitulée « Explication ligne par ligne »| Ligne | Code | Pourquoi |
|---|---|---|
= 0 | Spécificateur virtuelle pure | Rend la classe abstraite |
AAnimal | Préfixe A | Convention indiquant abstraite |
Pièges courants
Section intitulée « Pièges courants »1. Oublier de changer toutes les références à Animal
Mettez à jour toutes les classes dérivées pour hériter de AAnimal au lieu de Animal.
2. Essayer d’instancier une classe abstraite
// FAUX - erreur de compilation !AAnimal a;
// CORRECT - utiliser classe dérivéeDog d;AAnimal* ptr = new Dog();Conseils de test
Section intitulée « Conseils de test »int main() { // Ceci ne devrait PAS compiler : // AAnimal a; // Erreur : impossible d'instancier une classe abstraite
// Ceci devrait fonctionner : AAnimal* dog = new Dog(); AAnimal* cat = new Cat();
dog->makeSound(); // "Woof!" cat->makeSound(); // "Meow!"
delete dog; delete cat;
return 0;}Code final
Section intitulée « Code final »#ifndef AANIMAL_HPP#define AANIMAL_HPP
#include <string>
class AAnimal {protected: std::string _type;
public: AAnimal(); AAnimal(const AAnimal& other); AAnimal& operator=(const AAnimal& other); virtual ~AAnimal();
virtual void makeSound() const = 0; std::string getType() const;};
#endifExercice 03 : Interface & Récapitulatif
Section intitulée « Exercice 03 : Interface & Récapitulatif »Analyse du sujet
Section intitulée « Analyse du sujet »Implémentez un système Materia avec des interfaces :
- AMateria : Base abstraite pour les matériaux magiques (Ice, Cure)
- ICharacter : Interface pour les personnages qui peuvent équiper des materias
- IMateriaSource : Interface pour créer des materias
Fonctionnalités clés :
- Méthode
clone()pour dupliquer les materias - Les personnages ont 4 emplacements d’inventaire
- MateriaSource peut « apprendre » et « créer » des materias
Stratégie d’approche
Section intitulée « Stratégie d’approche »Étape 1 - Comprendre les interfaces
Une interface est une classe avec :
- UNIQUEMENT des fonctions virtuelles pures
- Destructeur virtuel
- PAS de variables membres (sauf constantes)
Étape 2 - Le patron clone
AMateria* Ice::clone() const { return new Ice(*this); // Retourner une copie de moi-même}Cela permet de créer des copies sans connaître le type concret.
Étape 3 - Règles de propriété de la mémoire
equip(): Le personnage prend possessionunequip(): Le personnage libère la possession (mais ne supprime pas !)createMateria(): L’appelant possède la materia retournée
Construction progressive du code
Section intitulée « Construction progressive du code »Étape 1 - Classe de base AMateria :
#ifndef AMATERIA_HPP#define AMATERIA_HPP
#include <string>
class ICharacter; // Déclaration anticipée
class AMateria {protected: std::string _type;
public: AMateria(std::string const& type); AMateria(const AMateria& other); AMateria& operator=(const AMateria& other); virtual ~AMateria();
std::string const& getType() const; virtual AMateria* clone() const = 0; // Virtuelle pure virtual void use(ICharacter& target);};
#endifÉtape 2 - Materia concrète (Ice) :
#ifndef ICE_HPP#define ICE_HPP
#include "AMateria.hpp"
class Ice : public AMateria {public: Ice(); Ice(const Ice& other); Ice& operator=(const Ice& other); ~Ice();
AMateria* clone() const; void use(ICharacter& target);};
#endif#include "Ice.hpp"#include "ICharacter.hpp"#include <iostream>
Ice::Ice() : AMateria("ice") {}
Ice::Ice(const Ice& other) : AMateria(other) {}
Ice& Ice::operator=(const Ice& other) { AMateria::operator=(other); return *this;}
Ice::~Ice() {}
AMateria* Ice::clone() const { return new Ice(*this); // Retourner nouvelle copie}
void Ice::use(ICharacter& target) { std::cout << "* shoots an ice bolt at " << target.getName() << " *" << std::endl;}Étape 3 - Interface ICharacter :
#ifndef ICHARACTER_HPP#define ICHARACTER_HPP
#include <string>
class AMateria;
class ICharacter {public: virtual ~ICharacter() {} virtual std::string const& getName() const = 0; virtual void equip(AMateria* m) = 0; virtual void unequip(int idx) = 0; virtual void use(int idx, ICharacter& target) = 0;};
#endifÉtape 4 - Implémentation Character :
#ifndef CHARACTER_HPP#define CHARACTER_HPP
#include "ICharacter.hpp"
class Character : public ICharacter {private: std::string _name; AMateria* _inventory[4];
public: Character(std::string name); Character(const Character& other); Character& operator=(const Character& other); ~Character();
std::string const& getName() const; void equip(AMateria* m); void unequip(int idx); void use(int idx, ICharacter& target);};
#endif#include "Character.hpp"#include "AMateria.hpp"#include <iostream>
Character::Character(std::string name) : _name(name) { for (int i = 0; i < 4; i++) _inventory[i] = NULL;}
Character::Character(const Character& other) : _name(other._name) { for (int i = 0; i < 4; i++) { if (other._inventory[i]) _inventory[i] = other._inventory[i]->clone(); // Copie profonde ! else _inventory[i] = NULL; }}
Character& Character::operator=(const Character& other) { if (this != &other) { _name = other._name; // Supprimer ancien inventaire for (int i = 0; i < 4; i++) delete _inventory[i]; // Cloner nouvel inventaire for (int i = 0; i < 4; i++) { if (other._inventory[i]) _inventory[i] = other._inventory[i]->clone(); else _inventory[i] = NULL; } } return *this;}
Character::~Character() { for (int i = 0; i < 4; i++) delete _inventory[i];}
std::string const& Character::getName() const { return _name;}
void Character::equip(AMateria* m) { if (!m) return; for (int i = 0; i < 4; i++) { if (!_inventory[i]) { _inventory[i] = m; return; } }}
void Character::unequip(int idx) { if (idx < 0 || idx >= 4) return; _inventory[idx] = NULL; // Ne pas supprimer ! Le sujet le dit.}
void Character::use(int idx, ICharacter& target) { if (idx < 0 || idx >= 4 || !_inventory[idx]) return; _inventory[idx]->use(target);}Étape 5 - MateriaSource :
AMateria* MateriaSource::createMateria(std::string const& type) { for (int i = 0; i < 4; i++) { if (_templates[i] && _templates[i]->getType() == type) return _templates[i]->clone(); // Retourner clone, pas l'original ! } return NULL; // Type inconnu}Explication ligne par ligne
Section intitulée « Explication ligne par ligne »| Ligne | Code | Pourquoi |
|---|---|---|
class ICharacter; | Déclaration anticipée | Éviter dépendance circulaire |
virtual ~ICharacter() {} | Destructeur virtuel | Requis pour les interfaces |
= 0 | Virtuelle pure | En fait une interface |
return new Ice(*this) | Patron clone | Créer copie sans connaître le type |
_inventory[idx] = NULL | unequip | Ne pas supprimer - exigence du sujet |
Pièges courants
Section intitulée « Pièges courants »1. Supprimer dans unequip()
// FAUX - le sujet dit de ne pas supprimer !void Character::unequip(int idx) { delete _inventory[idx]; // NON ! _inventory[idx] = NULL;}
// CORRECT - juste retirer de l'inventairevoid Character::unequip(int idx) { _inventory[idx] = NULL; // La materia abandonnée est le problème de l'appelant}2. Copie superficielle de l’inventaire
// FAUX - copie les pointeurs, pas les materias !for (int i = 0; i < 4; i++) _inventory[i] = other._inventory[i];
// CORRECT - cloner chaque materiafor (int i = 0; i < 4; i++) _inventory[i] = other._inventory[i] ? other._inventory[i]->clone() : NULL;3. Retourner l’original au lieu du clone dans createMateria
// FAUX - retourne le template lui-même !return _templates[i];
// CORRECT - retourner un nouveau clonereturn _templates[i]->clone();Conseils de test
Section intitulée « Conseils de test »int main() { IMateriaSource* src = new MateriaSource(); src->learnMateria(new Ice()); src->learnMateria(new Cure());
ICharacter* me = new Character("me");
AMateria* tmp; tmp = src->createMateria("ice"); me->equip(tmp); tmp = src->createMateria("cure"); me->equip(tmp);
ICharacter* bob = new Character("bob");
me->use(0, *bob); // "* shoots an ice bolt at bob *" me->use(1, *bob); // "* heals bob's wounds *"
delete bob; delete me; delete src;
return 0;}Code final
Section intitulée « Code final »#include "MateriaSource.hpp"
MateriaSource::MateriaSource() { for (int i = 0; i < 4; i++) _templates[i] = NULL;}
MateriaSource::MateriaSource(const MateriaSource& other) { for (int i = 0; i < 4; i++) { if (other._templates[i]) _templates[i] = other._templates[i]->clone(); else _templates[i] = NULL; }}
MateriaSource& MateriaSource::operator=(const MateriaSource& other) { if (this != &other) { for (int i = 0; i < 4; i++) delete _templates[i]; for (int i = 0; i < 4; i++) { if (other._templates[i]) _templates[i] = other._templates[i]->clone(); else _templates[i] = NULL; } } return *this;}
MateriaSource::~MateriaSource() { for (int i = 0; i < 4; i++) delete _templates[i];}
void MateriaSource::learnMateria(AMateria* m) { if (!m) return; for (int i = 0; i < 4; i++) { if (!_templates[i]) { _templates[i] = m; return; } }}
AMateria* MateriaSource::createMateria(std::string const& type) { for (int i = 0; i < 4; i++) { if (_templates[i] && _templates[i]->getType() == type) return _templates[i]->clone(); } return NULL;}Référence rapide
Section intitulée « Référence rapide »Syntaxe des fonctions virtuelles
Section intitulée « Syntaxe des fonctions virtuelles »virtual void method(); // Virtuelle (peut redéfinir)virtual void method() = 0; // Virtuelle pure (doit redéfinir)void method(); // Non-virtuelle (masque, ne redéfinit pas)Classe abstraite
Section intitulée « Classe abstraite »- A au moins une fonction virtuelle pure
- Ne peut pas être instanciée
- Peut avoir des membres de données et des méthodes non-pures
Interface
Section intitulée « Interface »- Uniquement des fonctions virtuelles pures
- Destructeur virtuel
- Pas de membres de données (typiquement)
- Définit un contrat
Liste de vérification sécurité mémoire
Section intitulée « Liste de vérification sécurité mémoire »- Destructeur virtuel dans la classe de base
- Copie profonde dans le constructeur de copie
- Copie profonde dans l’opérateur d’assignation
- Supprimer la mémoire allouée dans le destructeur
- Gérer unequip() sans supprimer (sauvegarder le pointeur d’abord)
Concepts connexes
Section intitulée « Concepts connexes »Continuez votre parcours C++ :
- Précédent : Module 03 : Héritage - Réviser classes de base/dérivées
- Suivant : Module 05 : Exceptions - Apprendre la gestion des erreurs avec try/catch/throw
Termes clés de ce module
Section intitulée « Termes clés de ce module »Visitez le Glossaire pour les définitions de :