Aller au contenu

Module 03 : Héritage

Télécharger le PDF officiel du sujet

Concepts clés :

  • Classes de base et dérivées
  • Spécificateurs d’accès avec l’héritage
  • Chaînage des constructeurs/destructeurs
  • Accès aux membres dans l’héritage
  • Héritage multiple (Problème du Diamant)

L’héritage est le fondement de la conception orientée objet. Il permet la réutilisation du code et établit des relations entre les types.


Pourquoi l’héritage ? (Le problème : Duplication du code)

Section intitulée « Pourquoi l’héritage ? (Le problème : Duplication du code) »

Imaginez que vous écrivez un jeu avec différents types de robots :

// Sans héritage - beaucoup de code dupliqué !
class ClapTrap {
std::string _name;
int _hitPoints;
int _energyPoints;
void attack(std::string target);
void takeDamage(int amount);
void beRepaired(int amount);
};
class ScavTrap {
std::string _name; // Dupliqué !
int _hitPoints; // Dupliqué !
int _energyPoints; // Dupliqué !
void attack(std::string target); // Logique similaire !
void takeDamage(int amount); // Dupliqué !
void beRepaired(int amount); // Dupliqué !
void guardGate(); // Seulement ceci est unique
};
class FragTrap {
std::string _name; // Dupliqué ENCORE !
// ... même problème de duplication
};

C’est pénible :

  • Même code écrit plusieurs fois
  • Les corrections de bugs doivent être appliquées à plusieurs endroits
  • Ajouter des fonctionnalités signifie modifier de nombreuses classes

L’héritage résout cela : Extraire le code commun dans une classe de base, puis dériver des classes spécialisées qui ajoutent des fonctionnalités uniques :

class ClapTrap { /* code commun */ };
class ScavTrap : public ClapTrap { void guardGate(); }; // Ajoute guardGate
class FragTrap : public ClapTrap { void highFivesGuys(); }; // Ajoute highFives

Comprendre la syntaxe de l’héritage (Pour débutants)

Section intitulée « Comprendre la syntaxe de l’héritage (Pour débutants) »

Ce module introduit l’héritage de classes. Voici les nouveaux opérateurs et syntaxes que vous rencontrerez :

class Derived : public Base { }; // Derived hérite de Base
  • : (deux-points) introduit la liste d’héritage
  • Cela signifie « cette classe EST-UN type de cette classe »
  • public signifie que les membres public de la classe de base restent public dans la classe dérivée
  • Lisez-le comme : « Derived hérite publiquement de Base »
class Middle : virtual public Base { }; // Héritage virtuel
  • virtual dans l’héritage résout le « problème du diamant »
  • Sans virtual, une classe héritant de deux classes qui partagent une base obtient DEUX copies de la base
  • Avec virtual, seulement UNE copie de la classe de base existe
  • Utilisez ceci quand l’héritage multiple crée une forme de diamant dans la hiérarchie des classes
ClapTrap::attack(target); // Appeler la version de la classe de base
Base::operator=(other); // Appeler l'opérateur d'assignation de la base
  • ClassName:: vous permet d’appeler explicitement une méthode de la classe de base
  • C’est nécessaire quand vous avez redéfini (overridé) une méthode dans la classe dérivée
  • Utilisez-le pour accéder à l’implémentation de la classe de base depuis la classe dérivée
using ScavTrap::attack; // Amener attack de ScavTrap dans la portée courante
using Base::print; // Amener TOUTES les surcharges de Base::print dans la portée
  • using importe des noms d’une autre portée dans la portée courante
  • Dans l’héritage, cela amène les méthodes de la classe de base dans la portée de la classe dérivée
  • Utile quand vous redéfinissez certaines surcharges mais voulez garder les autres accessibles
  • Sans using Base::print, si vous redéfinissez print() (sans paramètres), la surcharge print(int) devient masquée
class Base {
protected: // Accessible dans cette classe ET les classes dérivées
int _hitPoints;
public: // Accessible partout
void attack();
};
  • protected est entre private et public
  • Private : Seule la classe elle-même peut y accéder
  • Protected : La classe ET ses classes dérivées peuvent y accéder
  • Public : Tout le monde peut y accéder
  • Utilisez protected pour les membres que les classes dérivées doivent utiliser mais que les extérieurs ne devraient pas accéder

Chaînage des constructeurs avec liste d’initialisation

Section intitulée « Chaînage des constructeurs avec liste d’initialisation »
ScavTrap(std::string name) : ClapTrap(name) {
// Constructeur de base appelé en premier, puis ceci s'exécute
}
  • La liste d’initialisation (: avant le {) chaîne vers le constructeur de base
  • ClapTrap(name) appelle le constructeur de la classe de base avec le paramètre name
  • Le constructeur de la classe de base s’exécute AVANT le corps du constructeur de la classe dérivée
  • Vous DEVEZ appeler le constructeur de base si la base n’a pas de constructeur par défaut

Pourquoi la base s’exécute en premier ? Un objet dérivé contient l’objet de base « à l’intérieur ». La partie base doit être entièrement construite avant que la partie dérivée puisse l’utiliser. Pensez à construire une maison : vous avez besoin des fondations (base) avant de pouvoir ajouter les étages supérieurs (dérivée).

ScavTrap(const ScavTrap& other) : ClapTrap(other) {
// other est un ScavTrap, qui EST-UN ClapTrap
}
  • ClapTrap(other) appelle le constructeur de copie de base
  • Même si other est un ScavTrap, il peut être passé comme ClapTrap& car la relation d’héritage EST-UN
  • Cela copie les membres de la classe de base ; vous copiez les membres de la classe dérivée dans le corps

class Base {
protected:
std::string _name;
int _hitPoints;
public:
Base(std::string name);
void attack(const std::string& target);
};
class Derived : public ClapTrap { // Derived hérite de ClapTrap
public:
Derived(std::string name);
void specialAbility();
};
┌─────────────────────────────────────────────────────────────────┐
│ Héritage = Relation "EST-UN" │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Modèle visuel : │
│ ┌───────────────┐ │
│ │ ClapTrap │ ←── Classe de base (parent) │
│ │ ───────── │ Définit les attributs communs │
│ │ _name │ et les comportements │
│ │ _hitPoints │ │
│ │ attack() │ │
│ └───────┬───────┘ │
│ │ │
│ │ "EST-UN" │
│ │ │
│ ┌────┴────┐ │
│ ▼ ▼ │
│ ┌────────┐ ┌────────┐ │
│ │ScavTrap│ │FragTrap│ ←── Classes dérivées (enfants) │
│ │ EST-UN │ │ EST-UN │ Héritent de ClapTrap │
│ │ClapTrap│ │ClapTrap│ + ajoutent des spécialisations │
│ │────────│ │────────│ │
│ │guardG- │ │highF- │ │
│ │ate() │ │ives() │ │
│ └────────┘ └────────┘ │
│ │
│ Disposition mémoire : │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Partie ClapTrap (héritée) │ │
│ │ ┌─────────────┐ │ │
│ │ │ _name │ │ │
│ │ │ _hitPoints │ │ │
│ │ │ _energy │ ←── Données de la classe de base │ │
│ │ └─────────────┘ │ │
│ ├──────────────────────────────────────────────────────┤ │
│ │ Partie ScavTrap (ajoutée) │ │
│ │ ┌─────────────┐ │ │
│ │ │ _gateMode │ ←── Données de la classe dérivée │ │
│ │ └─────────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ Lisez-le comme : "ScavTrap EST-UN ClapTrap" │
│ - Un ScavTrap peut faire tout ce qu'un ClapTrap peut faire │
│ - Plus ScavTrap ajoute ses propres capacités spéciales │
│ - Utilisez l'héritage pour les relations "est-un" │
│ - Utilisez la composition pour les relations "a-un" │
│ │
└─────────────────────────────────────────────────────────────────┘
Type de membreHérité ?
Membres publicOui (comme public)
Membres protectedOui (comme protected)
Membres privateNon (existent mais inaccessibles)
ConstructeursNon (mais peuvent être appelés)
DestructeursNon (mais automatiquement appelés)

class Derived : public Base {
// Base public -> Derived public
// Base protected -> Derived protected
// Base private -> inaccessible
};
class Derived : protected Base {
// Base public -> Derived protected
// Base protected -> Derived protected
// Base private -> inaccessible
};
class Derived : private Base {
// Base public -> Derived private
// Base protected -> Derived private
// Base private -> inaccessible
};
Dans classe dérivée À l'extérieur
Héritage public :
Base public public accessible
Base protected protected non accessible
Base private - -
Héritage protected :
Base public protected non accessible
Base protected protected non accessible
Base private - -
Héritage private :
Base public private non accessible
Base protected private non accessible
Base private - -

  1. Le constructeur de la classe de base s’exécute EN PREMIER
  2. Le constructeur de la classe dérivée s’exécute EN SECOND
class ClapTrap {
public:
ClapTrap(std::string name) {
std::cout << "ClapTrap constructor" << std::endl;
}
};
class ScavTrap : public ClapTrap {
public:
ScavTrap(std::string name) : ClapTrap(name) {
std::cout << "ScavTrap constructor" << std::endl;
}
};
// Créer un ScavTrap affiche :
// ClapTrap constructor
// ScavTrap constructor
┌─────────────────────────────────────────────────────────────────┐
│ Ordre d'exécution des constructeurs dans l'héritage │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Ordre de construction (Haut vers bas) : │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌─────────────┐ │ │
│ │ │ 1. BASE │ ◄── Constructeur ClapTrap EN PREMIER │ │
│ │ │ CONSTRUCTEUR│ Initialise les membres hérités │ │
│ │ └──────┬──────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────┐ │ │
│ │ │ 2. MEMBRES │ ◄── Constructeurs des variables │ │
│ │ │ CONSTRUCTEURS│ (si des membres sont des objets) │ │
│ │ └──────┬──────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────┐ │ │
│ │ │ 3. DÉRIVÉE │ ◄── Corps du constructeur ScavTrap │ │
│ │ │ CONSTRUCTEUR│ Initialise ses propres membres │ │
│ │ └──────┬──────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ [OBJET PRÊT] │ │
│ │ │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Ordre de destruction (Bas vers haut - INVERSE) : │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌─────────────┐ │ │
│ │ │ 1. DÉRIVÉE │ ◄── Destructeur ScavTrap EN PREMIER │ │
│ │ │ DESTRUCTEUR │ Nettoie ses propres ressources │ │
│ │ └──────┬──────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────┐ │ │
│ │ │ 2. MEMBRES │ ◄── Destructeurs des membres │ │
│ │ │ DESTRUCTEURS│ │ │
│ │ └──────┬──────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────┐ │ │
│ │ │ 3. BASE │ ◄── Destructeur ClapTrap EN DERNIER │ │
│ │ │ DESTRUCTEUR │ Nettoie ressources héritées │ │
│ │ └─────────────┘ │ │
│ │ │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Rappelez-vous : Construction HAUT-BAS (base → dérivée) │
│ Destruction BAS-HAUT (dérivée → base) │
│ │
└─────────────────────────────────────────────────────────────────┘
  1. Le destructeur de la classe dérivée s’exécute EN PREMIER
  2. Le destructeur de la classe de base s’exécute EN SECOND
class ClapTrap {
public:
~ClapTrap() {
std::cout << "ClapTrap destructor" << std::endl;
}
};
class ScavTrap : public ClapTrap {
public:
~ScavTrap() {
std::cout << "ScavTrap destructor" << std::endl;
}
};
// Détruire un ScavTrap affiche :
// ScavTrap destructor
// ClapTrap destructor
class ScavTrap : public ClapTrap {
public:
// DOIT appeler le constructeur de base dans la liste d'initialisation
ScavTrap(std::string name) : ClapTrap(name) {
// Base est déjà construite ici
_hitPoints = 100; // Redéfinir les valeurs de base
_energyPoints = 50;
_attackDamage = 20;
}
};

Le constructeur de copie doit explicitement appeler le constructeur de copie de la classe de base :

class ScavTrap : public ClapTrap {
public:
// Constructeur de copie - appeler le constructeur de copie de base
ScavTrap(const ScavTrap& other) : ClapTrap(other) {
// other est un ScavTrap, qui EST-UN ClapTrap
// Le constructeur de copie de la classe de base gère les membres hérités
std::cout << "ScavTrap copy constructor" << std::endl;
}
};

Opérateur d’assignation dans les classes dérivées

Section intitulée « Opérateur d’assignation dans les classes dérivées »

L’opérateur d’assignation doit appeler l’opérateur d’assignation de la classe de base :

class ScavTrap : public ClapTrap {
public:
ScavTrap& operator=(const ScavTrap& other) {
if (this != &other) {
// Appeler l'opérateur d'assignation de la classe de base
ClapTrap::operator=(other);
// Assigner les membres de la classe dérivée (s'il y en a)
// _derivedMember = other._derivedMember;
}
return *this;
}
};

Forme canonique orthodoxe complète pour classe dérivée

Section intitulée « Forme canonique orthodoxe complète pour classe dérivée »
class ScavTrap : public ClapTrap {
public:
// Constructeur par défaut
ScavTrap() : ClapTrap() {
_hitPoints = 100;
_energyPoints = 50;
_attackDamage = 20;
}
// Constructeur paramétré
ScavTrap(std::string name) : ClapTrap(name) {
_hitPoints = 100;
_energyPoints = 50;
_attackDamage = 20;
}
// Constructeur de copie
ScavTrap(const ScavTrap& other) : ClapTrap(other) {
// Membres dérivés copiés ici s'il y en a
}
// Opérateur d'assignation
ScavTrap& operator=(const ScavTrap& other) {
if (this != &other) {
ClapTrap::operator=(other);
}
return *this;
}
// Destructeur
~ScavTrap() {
// Nettoyage dérivé (destructeur de base appelé automatiquement)
}
};

class ClapTrap {
public:
void attack(const std::string& target) {
std::cout << "ClapTrap " << _name << " attacks " << target << std::endl;
}
};
class ScavTrap : public ClapTrap {
public:
void attack(const std::string& target) {
std::cout << "ScavTrap " << _name << " attacks " << target << std::endl;
}
};
ClapTrap clap("Clappy");
ScavTrap scav("Scavvy");
clap.attack("enemy"); // "ClapTrap Clappy attacks enemy"
scav.attack("enemy"); // "ScavTrap Scavvy attacks enemy"
class ScavTrap : public ClapTrap {
public:
void attack(const std::string& target) {
// Appeler la version de la classe de base
ClapTrap::attack(target);
// Ajouter un comportement supplémentaire
std::cout << "...with extra ScavTrap power!" << std::endl;
}
};

Quand une classe dérivée déclare un membre avec le même nom qu’un membre de la classe de base, il masque (cache) le membre de la classe de base :

class Base {
public:
void print() { std::cout << "Base" << std::endl; }
void print(int x) { std::cout << "Base: " << x << std::endl; }
};
class Derived : public Base {
public:
void print() { std::cout << "Derived" << std::endl; }
// print(int) est maintenant MASQUÉ !
};
Derived d;
d.print(); // OK : "Derived"
d.print(42); // ERREUR : print(int) est masqué !
d.Base::print(42); // OK : résolution de portée explicite

Amener les membres de la classe de base dans la portée de la classe dérivée :

class DiamondTrap : public ScavTrap, public FragTrap {
public:
// Amener attack de ScavTrap dans la portée de DiamondTrap
using ScavTrap::attack;
// Maintenant DiamondTrap::attack() appelle ScavTrap::attack()
};
// Aussi utile pour dé-masquer les fonctions surchargées :
class Derived : public Base {
public:
using Base::print; // Amener TOUTES les surcharges de Base::print
void print() { std::cout << "Derived" << std::endl; }
};
Derived d;
d.print(); // OK : "Derived"
d.print(42); // OK : Base::print(int) est maintenant visible

class ClapTrap {
private:
std::string _name; // Seul ClapTrap peut y accéder
protected:
int _hitPoints; // ClapTrap ET les classes dérivées peuvent y accéder
public:
void display(); // Tout le monde peut y accéder
};
class ScavTrap : public ClapTrap {
public:
void specialAttack() {
// _name = "X"; // ERREUR : private dans ClapTrap
_hitPoints -= 10; // OK : protected est accessible
}
};
class Base {
private:
int _private; // Seulement les méthodes de Base
protected:
int _protected; // Méthodes de Base + Derived
public:
int _public; // Tout le monde
};

class ClapTrap {
protected:
std::string _name;
int _hitPoints; // 10
int _energyPoints; // 10
int _attackDamage; // 0
public:
ClapTrap(std::string name);
ClapTrap(const ClapTrap& other);
ClapTrap& operator=(const ClapTrap& other);
~ClapTrap();
void attack(const std::string& target);
void takeDamage(unsigned int amount);
void beRepaired(unsigned int amount);
};
class ScavTrap : public ClapTrap {
public:
ScavTrap(std::string name);
ScavTrap(const ScavTrap& other);
ScavTrap& operator=(const ScavTrap& other);
~ScavTrap();
void attack(const std::string& target); // Override
void guardGate(); // Nouvelle capacité
};
// Le constructeur doit initialiser la base avec des valeurs différentes :
// _hitPoints = 100, _energyPoints = 50, _attackDamage = 20
class FragTrap : public ClapTrap {
public:
FragTrap(std::string name);
FragTrap(const FragTrap& other);
FragTrap& operator=(const FragTrap& other);
~FragTrap();
void highFivesGuys(); // Nouvelle capacité
};
// _hitPoints = 100, _energyPoints = 100, _attackDamage = 30

7. Héritage multiple et le problème du diamant (ex03)

Section intitulée « 7. Héritage multiple et le problème du diamant (ex03) »
ClapTrap
/ \
ScavTrap FragTrap
\ /
DiamondTrap

Sans héritage virtuel :

  • DiamondTrap a DEUX copies de ClapTrap
  • Ambiguïté : quel _name de ClapTrap ?
class ClapTrap {
// ...
};
class ScavTrap : virtual public ClapTrap {
// ^^^^^^^ MOT CLÉ
};
class FragTrap : virtual public ClapTrap {
// ^^^^^^^ MOT CLÉ
};
class DiamondTrap : public ScavTrap, public FragTrap {
// Maintenant seulement UN sous-objet ClapTrap
};
class DiamondTrap : public ScavTrap, public FragTrap {
private:
std::string _name; // Même nom de variable que ClapTrap::_name
public:
DiamondTrap(std::string name);
~DiamondTrap();
// Utilise attack de ScavTrap
using ScavTrap::attack;
void whoAmI();
};
DiamondTrap::DiamondTrap(std::string name)
: ClapTrap(name + "_clap_name"), // Initialiser la base virtuelle
ScavTrap(name),
FragTrap(name),
_name(name)
{
// Attributs de FragTrap sauf energy de ScavTrap
_hitPoints = FragTrap::_hitPoints; // ou juste 100
_energyPoints = ScavTrap::_energyPoints; // ou juste 50
_attackDamage = FragTrap::_attackDamage; // ou juste 30
}
void DiamondTrap::whoAmI() {
std::cout << "I am " << _name << std::endl;
std::cout << "My ClapTrap name is " << ClapTrap::_name << std::endl;
}

Avec l’héritage virtuel, la classe LA PLUS DÉRIVÉE doit initialiser la base virtuelle :

DiamondTrap::DiamondTrap(std::string name)
: ClapTrap(name + "_clap_name"), // DiamondTrap initialise ClapTrap
ScavTrap(name), // L'init ClapTrap de ScavTrap est ignorée
FragTrap(name), // L'init ClapTrap de FragTrap est ignorée
_name(name)
{}

  • Relation « est-un » : ScavTrap EST UN ClapTrap
  • Réutilisation du code : Les classes dérivées partagent le code de la classe de base
  • Polymorphisme : Traiter la dérivée comme la base (Module 04)
  • Relation « a-un » : Utilisez la composition à la place
  • Juste pour la réutilisation de code avec des classes non liées
  • Quand la relation n’a pas de sens sémantique
  1. Oublier d’appeler le constructeur de base
  2. Mauvaises attentes sur l’ordre de destruction
  3. Accéder aux membres private (pas protected) de la base
  4. Ne pas utiliser l’héritage virtuel pour le diamant

Créez une classe de base ClapTrap - un petit robot qui peut :

  • attack : Inflige des dégâts, coûte 1 point d’énergie
  • takeDamage : Réduit les points de vie
  • beRepaired : Restaure les points de vie, coûte 1 point d’énergie

Attributs de départ :

AttributValeur
Name(paramètre du constructeur)
Hit points10
Energy points10
Attack damage0

Étape 1 - Planifier pour l’héritage

Même si cet exercice ne nécessite pas l’héritage, les suivants si. Utilisez protected pour les attributs afin que les classes dérivées puissent y accéder.

Étape 2 - Implémenter la vérification des ressources

Les actions nécessitent de l’énergie et des HP. Si l’un des deux est à zéro, l’action échoue.

Étape 3 - Suivre l’OCF

Incluez les quatre membres de la forme canonique orthodoxe avec des messages de constructeur/destructeur.

Étape 1 - Déclaration de classe :

ClapTrap.hpp
#ifndef CLAPTRAP_HPP
#define CLAPTRAP_HPP
#include <string>
class ClapTrap {
protected: // Protected, pas private - les classes dérivées ont besoin d'accès !
std::string _name;
int _hitPoints;
int _energyPoints;
int _attackDamage;
public:
ClapTrap(std::string name);
ClapTrap(const ClapTrap& other);
ClapTrap& operator=(const ClapTrap& other);
~ClapTrap();
void attack(const std::string& target);
void takeDamage(unsigned int amount);
void beRepaired(unsigned int amount);
};
#endif

Étape 2 - Constructeur avec messages :

ClapTrap.cpp
#include "ClapTrap.hpp"
#include <iostream>
ClapTrap::ClapTrap(std::string name)
: _name(name),
_hitPoints(10),
_energyPoints(10),
_attackDamage(0)
{
std::cout << "ClapTrap " << _name << " constructed" << std::endl;
}
ClapTrap::~ClapTrap() {
std::cout << "ClapTrap " << _name << " destructed" << std::endl;
}

Étape 3 - Actions avec vérification des ressources :

ClapTrap.cpp
void ClapTrap::attack(const std::string& target) {
if (_energyPoints <= 0 || _hitPoints <= 0) {
std::cout << "ClapTrap " << _name << " can't attack (no energy/HP)!" << std::endl;
return;
}
_energyPoints--;
std::cout << "ClapTrap " << _name << " attacks " << target
<< ", causing " << _attackDamage << " points of damage!" << std::endl;
}
void ClapTrap::takeDamage(unsigned int amount) {
_hitPoints -= amount;
if (_hitPoints < 0)
_hitPoints = 0;
std::cout << "ClapTrap " << _name << " takes " << amount
<< " damage! HP: " << _hitPoints << std::endl;
}
void ClapTrap::beRepaired(unsigned int amount) {
if (_energyPoints <= 0 || _hitPoints <= 0) {
std::cout << "ClapTrap " << _name << " can't repair (no energy/HP)!" << std::endl;
return;
}
_energyPoints--;
_hitPoints += amount;
std::cout << "ClapTrap " << _name << " repairs " << amount
<< " HP! HP: " << _hitPoints << std::endl;
}
LigneCodePourquoi
protected:Spécificateur d’accèsLes classes dérivées peuvent accéder, pas les autres
if (_energyPoints <= 0)Vérification ressourcesNe peut pas agir sans énergie
_energyPoints--Consommer ressourceLes actions coûtent de l’énergie
unsigned int amountParamètre non signéLes dégâts ne peuvent pas être négatifs

1. Utiliser private au lieu de protected

// FAUX - les classes dérivées ne peuvent pas accéder !
private:
std::string _name;
// CORRECT - les classes dérivées peuvent accéder
protected:
std::string _name;

2. Oublier les vérifications de ressources

// FAUX - peut attaquer sans énergie !
void ClapTrap::attack(const std::string& target) {
_energyPoints--; // Devient négatif !
// ...
}
// CORRECT - vérifier d'abord
void ClapTrap::attack(const std::string& target) {
if (_energyPoints <= 0 || _hitPoints <= 0) {
// Gérer l'échec
return;
}
_energyPoints--;
// ...
}
int main() {
ClapTrap a("Bob");
// Tester les actions de base
a.attack("Enemy");
a.takeDamage(5);
a.beRepaired(3);
// Tester l'épuisement des ressources
for (int i = 0; i < 12; i++) // Devrait échouer après 10
a.attack("Enemy");
return 0;
}
ClapTrap.cpp
#include "ClapTrap.hpp"
#include <iostream>
ClapTrap::ClapTrap(std::string name)
: _name(name), _hitPoints(10), _energyPoints(10), _attackDamage(0)
{
std::cout << "ClapTrap " << _name << " constructed" << std::endl;
}
ClapTrap::ClapTrap(const ClapTrap& other)
: _name(other._name), _hitPoints(other._hitPoints),
_energyPoints(other._energyPoints), _attackDamage(other._attackDamage)
{
std::cout << "ClapTrap " << _name << " copy constructed" << std::endl;
}
ClapTrap& ClapTrap::operator=(const ClapTrap& other) {
std::cout << "ClapTrap assignment operator called" << std::endl;
if (this != &other) {
_name = other._name;
_hitPoints = other._hitPoints;
_energyPoints = other._energyPoints;
_attackDamage = other._attackDamage;
}
return *this;
}
ClapTrap::~ClapTrap() {
std::cout << "ClapTrap " << _name << " destructed" << std::endl;
}
void ClapTrap::attack(const std::string& target) {
if (_energyPoints <= 0 || _hitPoints <= 0) {
std::cout << "ClapTrap " << _name << " can't attack!" << std::endl;
return;
}
_energyPoints--;
std::cout << "ClapTrap " << _name << " attacks " << target
<< ", causing " << _attackDamage << " damage!" << std::endl;
}
void ClapTrap::takeDamage(unsigned int amount) {
_hitPoints -= amount;
if (_hitPoints < 0) _hitPoints = 0;
std::cout << "ClapTrap " << _name << " takes " << amount << " damage!" << std::endl;
}
void ClapTrap::beRepaired(unsigned int amount) {
if (_energyPoints <= 0 || _hitPoints <= 0) {
std::cout << "ClapTrap " << _name << " can't repair!" << std::endl;
return;
}
_energyPoints--;
_hitPoints += amount;
std::cout << "ClapTrap " << _name << " repairs " << amount << " HP!" << std::endl;
}

Créez ScavTrap qui hérite de ClapTrap avec :

  • Valeurs d’attributs différentes :

    AttributValeur
    Hit points100
    Energy points50
    Attack damage20
  • Message d’attaque personnalisé

  • Nouvelle capacité : guardGate()

Étape 1 - Hériter de ClapTrap

Utilisez : public ClapTrap pour établir l’héritage.

Étape 2 - Chaînage des constructeurs

Appelez d’abord le constructeur de base, PUIS redéfinissez les valeurs.

Étape 3 - Comprendre l’ordre de construction

Lors de la création d’un ScavTrap :

  1. Le constructeur de ClapTrap s’exécute en premier
  2. Puis le constructeur de ScavTrap s’exécute

La destruction est inverse :

  1. Le destructeur de ScavTrap s’exécute en premier
  2. Puis le destructeur de ClapTrap s’exécute

Étape 1 - Déclaration de classe :

ScavTrap.hpp
#ifndef SCAVTRAP_HPP
#define SCAVTRAP_HPP
#include "ClapTrap.hpp"
class ScavTrap : public ClapTrap { // Hérite de ClapTrap
public:
ScavTrap(std::string name);
ScavTrap(const ScavTrap& other);
ScavTrap& operator=(const ScavTrap& other);
~ScavTrap();
void attack(const std::string& target); // Override l'attack de ClapTrap
void guardGate(); // Nouvelle capacité
};
#endif

Étape 2 - Constructeur avec initialisation de base :

ScavTrap.cpp
#include "ScavTrap.hpp"
#include <iostream>
ScavTrap::ScavTrap(std::string name) : ClapTrap(name) {
// Le constructeur de ClapTrap s'est déjà exécuté avec les valeurs par défaut
// Maintenant nous redéfinissons avec les valeurs de ScavTrap
_hitPoints = 100;
_energyPoints = 50;
_attackDamage = 20;
std::cout << "ScavTrap " << _name << " constructed" << std::endl;
}
ScavTrap::~ScavTrap() {
std::cout << "ScavTrap " << _name << " destructed" << std::endl;
}

Étape 3 - Override attack et ajouter nouvelle capacité :

ScavTrap.cpp
void ScavTrap::attack(const std::string& target) {
if (_energyPoints <= 0 || _hitPoints <= 0) {
std::cout << "ScavTrap " << _name << " can't attack!" << std::endl;
return;
}
_energyPoints--;
std::cout << "ScavTrap " << _name << " attacks " << target
<< ", causing " << _attackDamage << " damage!" << std::endl;
}
void ScavTrap::guardGate() {
std::cout << "ScavTrap " << _name << " is now in Gate keeper mode" << std::endl;
}
LigneCodePourquoi
: public ClapTrapHéritage publicScavTrap EST-UN ClapTrap
: ClapTrap(name)Appel constructeur de baseInitialiser la partie ClapTrap d’abord
_hitPoints = 100Redéfinir valeurScavTrap a des stats différentes
void attack(...)Override fonctionScavTrap a un message d’attaque personnalisé

1. Ne pas appeler le constructeur de base

// FAUX - ClapTrap pas correctement initialisé !
ScavTrap::ScavTrap(std::string name) {
_name = name;
_hitPoints = 100;
// ...
}
// CORRECT - appeler le constructeur de base d'abord
ScavTrap::ScavTrap(std::string name) : ClapTrap(name) {
_hitPoints = 100;
// ...
}

2. Définir les valeurs dans la liste d’initialisation (ne redéfinira pas)

// FAUX - le constructeur de base les a déjà définis !
ScavTrap::ScavTrap(std::string name)
: ClapTrap(name), _hitPoints(100) { // Erreur : ne peut pas init dans dérivée
}
// CORRECT - redéfinir dans le corps du constructeur
ScavTrap::ScavTrap(std::string name) : ClapTrap(name) {
_hitPoints = 100; // Redéfinir après construction de base
}
int main() {
std::cout << "=== Creating ScavTrap ===" << std::endl;
ScavTrap s("Scav");
s.attack("Target"); // Devrait montrer le message ScavTrap
s.guardGate(); // Capacité exclusive à ScavTrap
std::cout << "=== End of main ===" << std::endl;
return 0;
}
// Sortie attendue :
// ClapTrap Scav constructed
// ScavTrap Scav constructed
// ScavTrap Scav attacks Target, causing 20 damage!
// ScavTrap Scav is now in Gate keeper mode
// ScavTrap Scav destructed
// ClapTrap Scav destructed
ScavTrap.cpp
#include "ScavTrap.hpp"
#include <iostream>
ScavTrap::ScavTrap(std::string name) : ClapTrap(name) {
_hitPoints = 100;
_energyPoints = 50;
_attackDamage = 20;
std::cout << "ScavTrap " << _name << " constructed" << std::endl;
}
ScavTrap::ScavTrap(const ScavTrap& other) : ClapTrap(other) {
std::cout << "ScavTrap " << _name << " copy constructed" << std::endl;
}
ScavTrap& ScavTrap::operator=(const ScavTrap& other) {
ClapTrap::operator=(other);
return *this;
}
ScavTrap::~ScavTrap() {
std::cout << "ScavTrap " << _name << " destructed" << std::endl;
}
void ScavTrap::attack(const std::string& target) {
if (_energyPoints <= 0 || _hitPoints <= 0) {
std::cout << "ScavTrap " << _name << " can't attack!" << std::endl;
return;
}
_energyPoints--;
std::cout << "ScavTrap " << _name << " attacks " << target
<< ", causing " << _attackDamage << " damage!" << std::endl;
}
void ScavTrap::guardGate() {
std::cout << "ScavTrap " << _name << " is now in Gate keeper mode" << std::endl;
}

Créez FragTrap qui hérite de ClapTrap avec :

AttributValeur
Hit points100
Energy points100
Attack damage30

Nouvelle capacité : highFivesGuys() - demande un high five.

Même schéma que ScavTrap - cet exercice renforce les concepts d’héritage.

FragTrap.hpp
#ifndef FRAGTRAP_HPP
#define FRAGTRAP_HPP
#include "ClapTrap.hpp"
class FragTrap : public ClapTrap {
public:
FragTrap(std::string name);
FragTrap(const FragTrap& other);
FragTrap& operator=(const FragTrap& other);
~FragTrap();
void highFivesGuys();
};
#endif
FragTrap.cpp
#include "FragTrap.hpp"
#include <iostream>
FragTrap::FragTrap(std::string name) : ClapTrap(name) {
_hitPoints = 100;
_energyPoints = 100;
_attackDamage = 30;
std::cout << "FragTrap " << _name << " constructed" << std::endl;
}
FragTrap::FragTrap(const FragTrap& other) : ClapTrap(other) {
std::cout << "FragTrap " << _name << " copy constructed" << std::endl;
}
FragTrap& FragTrap::operator=(const FragTrap& other) {
ClapTrap::operator=(other);
return *this;
}
FragTrap::~FragTrap() {
std::cout << "FragTrap " << _name << " destructed" << std::endl;
}
void FragTrap::highFivesGuys() {
std::cout << "FragTrap " << _name << " requests a high five!" << std::endl;
}

Identiques à ScavTrap - assurez-vous d’appeler le constructeur de base !

int main() {
FragTrap f("Frag");
f.attack("Enemy"); // Utilise ClapTrap::attack (pas redéfini)
f.highFivesGuys(); // Capacité exclusive à FragTrap
return 0;
}

Créez DiamondTrap qui hérite À LA FOIS de ScavTrap ET FragTrap :

ClapTrap
/ \
ScavTrap FragTrap
\ /
DiamondTrap

Cela crée le problème du diamant : sans gestion spéciale, DiamondTrap aurait DEUX copies de ClapTrap !

Exigences :

  • Name : Propre nom de DiamondTrap, nom ClapTrap = name + “_clap_name”
  • HP : Ceux de FragTrap (100)
  • Energy : Ceux de ScavTrap (50)
  • Damage : Ceux de FragTrap (30)
  • attack() : Celui de ScavTrap
  • whoAmI() : Affiche les deux noms

Étape 1 - Comprendre le problème du diamant

Sans virtual :

DiamondTrap a :
- ScavTrap::ClapTrap (une copie)
- FragTrap::ClapTrap (une autre copie !)

Avec virtual :

DiamondTrap a :
- UN ClapTrap partagé (la bonne façon)

Étape 2 - Ajouter virtual aux classes intermédiaires

ScavTrap et FragTrap doivent tous deux utiliser virtual public ClapTrap.

Étape 3 - Initialiser la base virtuelle dans la classe la plus dérivée

Avec l’héritage virtuel, la classe LA PLUS DÉRIVÉE (DiamondTrap) initialise la base virtuelle (ClapTrap).

Étape 1 - Modifier ScavTrap et FragTrap (ajouter virtual) :

ScavTrap.hpp
class ScavTrap : virtual public ClapTrap { // Ajouter virtual
// ... reste inchangé
};
FragTrap.hpp
class FragTrap : virtual public ClapTrap { // Ajouter virtual
// ... reste inchangé
};

Étape 2 - Déclaration de DiamondTrap :

DiamondTrap.hpp
#ifndef DIAMONDTRAP_HPP
#define DIAMONDTRAP_HPP
#include "ScavTrap.hpp"
#include "FragTrap.hpp"
class DiamondTrap : public ScavTrap, public FragTrap {
private:
std::string _name; // Propre nom de DiamondTrap (masque ClapTrap::_name)
public:
DiamondTrap(std::string name);
DiamondTrap(const DiamondTrap& other);
DiamondTrap& operator=(const DiamondTrap& other);
~DiamondTrap();
using ScavTrap::attack; // Utiliser attack de ScavTrap
void whoAmI();
};
#endif

Étape 3 - Implémentation de DiamondTrap :

DiamondTrap.cpp
#include "DiamondTrap.hpp"
#include <iostream>
DiamondTrap::DiamondTrap(std::string name)
: ClapTrap(name + "_clap_name"), // Initialiser la base virtuelle EN PREMIER
ScavTrap(name),
FragTrap(name),
_name(name)
{
// Prendre des attributs spécifiques de chaque parent
_hitPoints = FragTrap::_hitPoints; // 100 de FragTrap
_energyPoints = ScavTrap::_energyPoints; // 50 de ScavTrap
_attackDamage = FragTrap::_attackDamage; // 30 de FragTrap
std::cout << "DiamondTrap " << _name << " constructed" << std::endl;
}
DiamondTrap::~DiamondTrap() {
std::cout << "DiamondTrap " << _name << " destructed" << std::endl;
}
void DiamondTrap::whoAmI() {
std::cout << "I am " << _name << std::endl;
std::cout << "My ClapTrap name is " << ClapTrap::_name << std::endl;
}
LigneCodePourquoi
virtual public ClapTrapHéritage virtuelEmpêche le ClapTrap dupliqué
: ClapTrap(name + "_clap_name")Init base virtuelleLa classe la plus dérivée initialise la base virtuelle
std::string _namePropre membreMasque ClapTrap::_name
using ScavTrap::attackRésoudre ambiguïtéChoisir explicitement attack de ScavTrap
ClapTrap::_nameRésolution de portéeAccéder au membre de la classe de base

1. Oublier virtual sur les classes intermédiaires

// FAUX - deux copies de ClapTrap !
class ScavTrap : public ClapTrap { };
class FragTrap : public ClapTrap { };
// CORRECT - un ClapTrap partagé
class ScavTrap : virtual public ClapTrap { };
class FragTrap : virtual public ClapTrap { };

2. Ne pas initialiser la base virtuelle dans DiamondTrap

// FAUX - ClapTrap pas correctement initialisé !
DiamondTrap::DiamondTrap(std::string name)
: ScavTrap(name), FragTrap(name) { }
// CORRECT - doit initialiser ClapTrap explicitement
DiamondTrap::DiamondTrap(std::string name)
: ClapTrap(name + "_clap_name"),
ScavTrap(name),
FragTrap(name) { }

3. Confondre les deux membres _name

void DiamondTrap::whoAmI() {
// _name est le propre membre de DiamondTrap
// ClapTrap::_name est le membre de la classe de base
std::cout << _name << std::endl; // Nom DiamondTrap
std::cout << ClapTrap::_name << std::endl; // Nom ClapTrap (avec suffixe)
}
int main() {
std::cout << "=== Creating DiamondTrap ===" << std::endl;
DiamondTrap d("Diamond");
d.whoAmI(); // Montre les deux noms
d.attack("Enemy"); // Utilise attack de ScavTrap
d.guardGate(); // Hérité de ScavTrap
d.highFivesGuys(); // Hérité de FragTrap
std::cout << "=== End of main ===" << std::endl;
return 0;
}
// Ordre de construction attendu :
// ClapTrap Diamond_clap_name constructed
// ScavTrap Diamond constructed
// FragTrap Diamond constructed
// DiamondTrap Diamond constructed
//
// Ordre de destruction attendu (inverse) :
// DiamondTrap Diamond destructed
// FragTrap Diamond destructed
// ScavTrap Diamond destructed
// ClapTrap Diamond_clap_name destructed
DiamondTrap.cpp
#include "DiamondTrap.hpp"
#include <iostream>
DiamondTrap::DiamondTrap(std::string name)
: ClapTrap(name + "_clap_name"),
ScavTrap(name),
FragTrap(name),
_name(name)
{
_hitPoints = FragTrap::_hitPoints;
_energyPoints = ScavTrap::_energyPoints;
_attackDamage = FragTrap::_attackDamage;
std::cout << "DiamondTrap " << _name << " constructed" << std::endl;
}
DiamondTrap::DiamondTrap(const DiamondTrap& other)
: ClapTrap(other),
ScavTrap(other),
FragTrap(other),
_name(other._name)
{
std::cout << "DiamondTrap " << _name << " copy constructed" << std::endl;
}
DiamondTrap& DiamondTrap::operator=(const DiamondTrap& other) {
ClapTrap::operator=(other);
_name = other._name;
return *this;
}
DiamondTrap::~DiamondTrap() {
std::cout << "DiamondTrap " << _name << " destructed" << std::endl;
}
void DiamondTrap::whoAmI() {
std::cout << "I am " << _name << std::endl;
std::cout << "My ClapTrap name is " << ClapTrap::_name << std::endl;
}

// Héritage de base
class Derived : public Base { };
// Chaînage des constructeurs
Derived(args) : Base(base_args), _member(val) { }
// Override fonction
void Derived::method() { Base::method(); /* extra */ }
// Héritage virtuel (diamant)
class Middle : virtual public Base { };

Continuez votre parcours C++ :

Visitez le Glossaire pour les définitions de :