Aller au contenu

Module 04 : Polymorphisme, classes abstraites et interfaces

Télécharger le PDF officiel du sujet

Concepts clés :

  • Fonctions virtuelles
  • Polymorphisme à l’exécution
  • Destructeurs virtuels
  • Classes abstraites (virtuelles pures)
  • Interfaces
  • Copie profonde avec polymorphisme

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 Animal
class Cat : public Animal { }; // Un Cat EST-UN Animal

Parce qu’un Dog EST-UN Animal, vous pouvez traiter les Dogs comme des Animals :

Animal* pet = new Dog(); // Valide ! Un Dog EST-UN Animal

C’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*.


Ce module introduit les fonctions virtuelles et les classes abstraites. Voici la syntaxe expliquée :

virtual void makeSound() { /* ... */ }
  • virtual avant 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’appeler Dog::makeSound() s’il pointe réellement vers un Dog
virtual void makeSound() = 0; // Fonction virtuelle pure
  • = 0 rend 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
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 pointeur
  • virtual assure que la bonne fonction (dérivée) est appelée
  • Sans virtual, vous obtiendriez toujours la version de la classe de base
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)
Animal* pet = new Dog();
delete pet; // Sans destructeur virtuel, le destructeur de Dog ne s'exécute jamais !
  • delete sur 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 pet appelle ~Dog() puis ~Animal() - correct !
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
dynamic_cast<Dog*>(animal); // Cast sécurisé à l'exécution
  • dynamic_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)

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()

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écution
// 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ée
┌─────────────────────────────────────────────────────────────────┐
│ 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 │
│ │
└─────────────────────────────────────────────────────────────────┘

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 !
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.


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 appeler makeSound() 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
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 abstraite
Animal* ptr; // OK : peut avoir un pointeur vers une classe abstraite
ptr = new Dog(); // OK : peut pointer vers une classe dérivée concrète
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)
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
};

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;
};
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);
};

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 base
Animal* original = new Dog();
Animal* copy = new Animal(*original); // Crée un Animal, pas un Dog !
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 !
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);
}
};
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];
}
};

// 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 polymorphique
for (int i = 0; i < 4; i++) {
animals[i]->makeSound(); // Appelle la bonne version
}
// CRITIQUE : Doit supprimer chaque élément
for (int i = 0; i < 4; i++) {
delete animals[i]; // Le destructeur virtuel assure le bon nettoyage
}
// SANS destructeur virtuel :
Animal* pet = new Dog(); // Dog alloue Brain
delete pet; // Seul ~Animal() appelé - Brain fuit !
// AVEC destructeur virtuel :
Animal* pet = new Dog(); // Dog alloue Brain
delete pet; // ~Dog() appelé en premier (supprime Brain), puis ~Animal()

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;
}
};
IMateriaSource* src = new MateriaSource();
// Enseigner à la factory ce qu'elle peut créer
src->learnMateria(new Ice());
src->learnMateria(new Cure());
// La factory crée de nouvelles instances
AMateria* ice = src->createMateria("ice"); // Nouvel objet Ice
AMateria* cure = src->createMateria("cure"); // Nouvel objet Cure
AMateria* unknown = src->createMateria("fire"); // NULL - pas appris
delete src;
  1. Découple la création de l’utilisation : Le client n’a pas besoin de connaître les types concrets
  2. Utilise clone() : Les nouveaux objets sont des copies des templates
  3. Propriété de la mémoire : La Factory possède les templates, l’appelant possède les objets créés

Créez une classe de base Animal avec des classes dérivées Dog et Cat :

  • Animal a un attribut type et une méthode makeSound()
  • Dog dit “Woof!”, Cat dit “Meow!”
  • La clé : Quand vous utilisez un pointeur Animal* vers un Dog, appeler makeSound() devrait afficher “Woof!”, pas un son générique

Créez aussi des versions “Wrong” pour démontrer ce qui se passe sans polymorphisme.

É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.

Étape 1 - Classe de base avec fonctions virtuelles :

Animal.hpp
#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 :

Dog.hpp
#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
Dog.cpp
#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) :

WrongAnimal.hpp
class WrongAnimal {
protected:
std::string _type;
public:
WrongAnimal();
~WrongAnimal(); // PAS virtuel !
void makeSound() const; // PAS virtuel !
std::string getType() const;
};
LigneCodePourquoi
virtual ~Animal()Destructeur virtuelAssure que le destructeur dérivé est appelé via pointeur base
virtual void makeSound() constFonction virtuelleActive le polymorphisme à l’exécution
void makeSound() const (in Dog)OverrideFournit l’implémentation spécifique à Dog
_type = "Dog"Définir type dérivéIdentifier le type réel

1. Oublier le mot-clé virtual

// FAUX - pas de polymorphisme
class Animal {
public:
void makeSound() const; // Pas virtuel
};
Animal* pet = new Dog();
pet->makeSound(); // Appelle Animal::makeSound(), PAS Dog !
// CORRECT - polymorphique
class Animal {
public:
virtual void makeSound() const;
};

2. Un destructeur non-virtuel cause des fuites mémoire

// FAUX
class Animal {
public:
~Animal(); // PAS virtuel
};
Animal* pet = new Dog();
delete pet; // Seul ~Animal() appelé ! ~Dog() sauté !
// CORRECT
class Animal {
public:
virtual ~Animal(); // Destructeur virtuel
};
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;
}
Animal.cpp
#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 »

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.

É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.

Étape 1 - Classe Brain :

Brain.hpp
#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
Brain.cpp
#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) :

Dog.hpp
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.cpp
Dog::Dog() : Animal() {
_type = "Dog";
_brain = new Brain(); // Allouer Brain
std::cout << "Dog constructed" << std::endl;
}
// Copie profonde - créer NOUVEAU Brain
Dog::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 profonde
Dog& 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;
}
LigneCodePourquoi
Brain* _brainPointeur vers BrainAllocation dynamique nécessaire
_brain = new Brain()Allouer dans constructeurChaque Dog a son propre Brain
_brain = new Brain(*other._brain)Copie profondeCréer NOUVEAU Brain avec même contenu
delete _brainDans assignationLibérer ANCIEN Brain avant nouvelle allocation
delete _brainDans destructeurNettoyer la mémoire

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 Brain
Dog::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 Brain
delete 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;
}
// CORRECT
Dog& Dog::operator=(const Dog& other) {
delete _brain; // Libérer ancien
_brain = new Brain(*other._brain);
return *this;
}
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;
}
Dog.cpp
#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;
}

Rendez Animal abstraite - elle ne devrait pas être instanciable directement.

  • Renommez Animal en AAnimal (le préfixe A indique abstraite)
  • Rendez makeSound() une fonction virtuelle pure
  • AAnimal a; devrait maintenant être une erreur de compilation

É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
AAnimal.hpp
#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;
};
#endif
LigneCodePourquoi
= 0Spécificateur virtuelle pureRend la classe abstraite
AAnimalPréfixe AConvention indiquant abstraite

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ée
Dog d;
AAnimal* ptr = new Dog();
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;
}
AAnimal.hpp
#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;
};
#endif

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

É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 possession
  • unequip() : Le personnage libère la possession (mais ne supprime pas !)
  • createMateria() : L’appelant possède la materia retournée

Étape 1 - Classe de base AMateria :

AMateria.hpp
#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) :

Ice.hpp
#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
Ice.cpp
#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 :

ICharacter.hpp
#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 :

Character.hpp
#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
Character.cpp
#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 :

MateriaSource.cpp
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
}
LigneCodePourquoi
class ICharacter;Déclaration anticipéeÉviter dépendance circulaire
virtual ~ICharacter() {}Destructeur virtuelRequis pour les interfaces
= 0Virtuelle pureEn fait une interface
return new Ice(*this)Patron cloneCréer copie sans connaître le type
_inventory[idx] = NULLunequipNe pas supprimer - exigence du sujet

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'inventaire
void 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 materia
for (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 clone
return _templates[i]->clone();
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;
}
MateriaSource.cpp
#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;
}

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)
  • 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
  • Uniquement des fonctions virtuelles pures
  • Destructeur virtuel
  • Pas de membres de données (typiquement)
  • Définit un contrat
  • 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)

Continuez votre parcours C++ :

Visitez le Glossaire pour les définitions de :