Logo HE-ARC Logo HES-SO

1242.2 Langage C++ - 2025-2026

Chapitre 4

Héritage et polymorphisme

1. Classes dérivées

2. Contrôle d'accès

3. Mode de dérivation

4. Hiérarchie de classes

5. Conversions de types

6. Fonctions virtuelles et Polymorphisme

7. Classes abstraites

4.1 Classes dérivées

Classe dérivées

4.1 Classes dérivées

Classe dérivées

4.1 Classes dérivées

Classe dérivées

4.1 Classes dérivées

Héritage

Création d'une nouvelle classe à partir d'une classe existante

Classe existante : on parle de classe de base, de classe mère

Nouvelle classe : on parle de classe dérivée, de classe fille


La classe dérivée hérite des membres de la classe de base

On peut :

  • ajouter des nouveaux membres (attributs et méthodes)
  • redéfinir des méthodes existantes (spécialiser)

Permet notamment d'éviter la répétition de code

4.1 Généralisation vs spécialisation

Généralisation vs spécialisation

4.1 Généralisation vs spécialisation

Généralisation

En UML, on va dans le sens de la généralisation

La flèche indique donc une généralisation

Chaque niveau d'abstraction devient de plus en plus général

Factorisation des éléments communs à une classe

Démarche complexe et itérative

4.1 Généralisation vs spécialisation

Spécialisation

La classe dérivée est une spécialisation de la classe de base

La classe dérivée possède des attributs et des méthodes propres

La classe dérivée peut redéfinir des méthodes de la classe de base

4.1 Exemples

Exemple de classe de base : Circle


            
            // Circle.h
            #pragma once

            class Circle
            {
              public:
                Circle(double r=0) : m_r(r){}
                double getRadius() const {return m_r;}
                void setRadius(double r) {m_r=r;}
                double surface() const {return m_r*m_r*std::numbers::pi;}

              private:
                double m_r{0};
            };
          

4.1 Exemples

Exemple de classe dérivée : Cylindre



            // Cylinder.h
            #pragma once

            #include "Circle.h"
            
            class Cylinder : public Circle
            {
              public:
                Cylinder(double=0, double=0);
                double surface() const;
                double volume() const;

              private:
                double m_h{0};
            };
          

4.1 Remarques

Héritage ⇒ une instance de Cylinder dispose de tous les membres de la classe Circle, avec :

  • 1 attribut supplémentaire m_h (hauteur du cylindre)
  • 1 nouvelle méthode volume()
  • 1 méthode redéfinie surface() (formule différente)

La méthode surface() a été redéfinie (pas une surcharge)


Comme la méthode Circle::surface() a la même signature que Cylinder::surface(), elle n'est plus directement accessible dans la classe Cylinder

⇒ Il faut explicitement utiliser Circle::surface()

4.1 Exemples

Implémentation de la classe Cylinder



            // Cylinder.cpp
            #include "cylinder.h"

            Cylinder::Cylinder(double radius, double height)
                : Circle(radius), m_h(height)
            {
            }

            double Cylinder::surface() const
            {
                double r = this->getRadius(); // Circle::m_r is private
                return 2 * std::numbers::pi * r * (r + m_h);
            }
            

4.1 Remarques

Comme l'attribut m_r a été déclaré private dans Circle, la classe Cylinder n'a pas accès à ce membre (bien qu'il soit présent, car hérité de Circle).

Un droit d'accès supplémentaire a été introduit pour pallier ce problème : protected

4.2 Contrôle d'accès protected

Un membre protected sera considéré comme :

  • public pour la classe dérivée
  • private pour les autres utilisateurs


Le contrôle d'accès protected facilite l'implémentation de classes dérivées. Il permet aux classes dérivées uniquement d'accéder aux membres hérités.

4.2 Exemples

Nouvelle classe de base Circle


            
            // Circle.h
            #pragma once

            class Circle
            {
              public:
                Circle(double r=0) : m_r(r){}
                double getRadius() const {return m_r;}
                void setRadius(double r) {m_r = r;}
                double surface() const {return m_r*m_r*std::numbers::pi;}

              protected:
                double m_r{0};
            };
          

4.2 Exemples

Nouvelle implémentation de la classe Cylinder



            // Cylinder.cpp
            #include "cylinder.h"

            Cylinder::Cylinder(double radius, double height)
                : Circle(radius), m_h(height)
            {
            }

            double Cylinder::surface() const
            {
              // m_r is now accessible
              return 2 * std::numbers::pi * m_r * (m_r + m_h); 
            }
            

4.3 Mode de dérivation

Le mode de dérivation permet de définir l'accessibilité des membres hérités. Il est précisé avant le nom de la superclasse.


class Cylinder : public Circle {...}

⚠️ IMPORTANT : l'héritage n'augmente jamais l'accessibilité d'un membre !

Dans tous les cas, les membres private de la classe de base seront inaccessibles, mais présents dans la classe dérivée

Modes de dérivation

  • public : les membres conservent leur accessibilité
  • protected : les membres publics deviennent protégés
  • private : les membres publics et protégés deviennent privés

4.3 Mode de dérivation

Dérivation Classe de base Classe dérivée
public public
protected
private
public
protected
Inaccessible
protected public
protected
private
protected
protected
Inaccessible
private public
protected
private
private
private
Inaccessible

4.4 Création / Destruction des objets

Lors de l'instanciation d'une classe dérivée, un objet de la classe de base est d'abord alloué et initialisé (appel d'un constructeur, celui par défaut si rien n'est précisé)

À la destruction d'un objet, cette séquence est inversée (exécution destructeur, libération objet dérivé, libération objet de base)



            {
              auto c = Cylinder(1, 2);
              // 1) Memory Allocation
              // 2) Call to Circle constructor
              // 3) Call to Cylinder constructor  
              ...
            }
            // 1) Call to Cylinder destructor
            // 2) Call to Circle destructor
            // 3) Memory deallocation
          

4.4 Création / Destruction des objets

Appel explicite des constructeurs de base



            // Cylinder.cpp
            #include "cylinder.h"

            Cylinder::Cylinder(double r, double h) : Circle(r)
            {
              m_h = h;
            }
          

Appel implicite des constructeurs de base



            // Cylinder.cpp
            #include "cylinder.h"

            Cylinder::Cylinder(double r, double h) // Call to Circle()
            {              
              m_r = r; // ⛔ r is private in Circle 
              m_h = h;
            }
          

4.4 Hiérarchie de classes

On peut faire plusieurs dérivations successives

Dans ce cas, la classe de base commune à toutes les classes est appelée classe racine

L'intérêt est que toutes les classes héritant de cette classe racine disposeront de ses membres

Disposer d'une classe racine permet d'utiliser le polymorphisme (voir plus loin)

4.4 Accès aux méthode de la classe de base

L'accès aux méthodes de la classe de base dépend du mode de dérivation (public, protected, ou private)

Si une méthode est redéfinie dans une classe dérivée, celle de la superclasse est alors masquée mais peut toujours être appelée avec Baseclass::baseMethod();

Cette syntaxe est utile pour utiliser le code de la classe de base avant d'exécuter celui propre à la classe dérivée

Pour les constructeurs, on utilise la syntaxe des listes d'initialisation: s'il n'y a aucun appel explicite, c'est le constructeur par défaut qui sera appelé

4.5 Conversions standard Base ⟺ Dérivée

Si la classe D dérive publiquement de la classe B, alors :

⇒ les membres de B sont membres de D

⇒ les membres public et protected de B peuvent être atteints à travers un objet D

⇒ toutes les fonctions non private, disponibles pour B le sont aussi pour D


Que peut-on dire de la conversion entre les objets instanciés de ces classes ?

4.5 Conversions standard Base ⟺ Dérivée



                class Base
                {
                public:
                  Base() = default;
                  Base(const Base&) = default;
                  Base &operator=(const Base&) = default;
                private:
                  int m_b{0};
                };
                
                class Derived : public Base
                {
                public:
                  Derived() = default;
                  Derived(const Derived&) = default;
                  Derived &operator=(const Derived&) = default;
                private:
                  int m_d{0};
                };
                


                int main()
                {
                  Derived D0;
                  // ✅ OK
                  Base B0 = D0;
                  // ✅ OK
                  B0 = D0;
                
                  Base B1;
                  // ⛔ Not enough information
                  Derived D1 = B1;
                  // ⛔ Not enough information
                  D1 = B1;
                
                  return 0;
                }
                

4.5 Conversions standard Base ⟺ Dérivée

Conversion Base ⟺ Dérivée


                cat = dog; // ⛔
                dog = cat; // ⛔
                animal = cat; // ✅
                cat = animal; // ⛔
                cat = straycat; // ✅
                straycat = cat; // ⛔
                animal = straycat; // ✅
                

4.5 Conversions standard Base ⟺ Dérivée

✅ BaseClass = DerivedClass;

⛔ DerivedClass = BaseClass;


On peut cependant rajouter la conversion en implémentant un constructeur de conversion Derived(const Base&)


⚠️ ATTENTION : en pratique, ce genre de conversions (on parle de Slicing) est fortement déconseillé

4.5 Conversions standard Base* ⟺ Dérivée*

Même chose que pour les objets

⇒ les pointeurs de Base peuvent pointer vers des objets de type Derived

⇒ les pointeurs de Derived ne peuvent pas pointer vers des objets de type Base



            int main()
            {
              Base* ptrBase0 = new Base();
              Derived* ptrDerived0 = new Derived();
            
              Base *ptrBase1 = ptrDerived0; // ✅
            
              Derived *ptrDerived1 = ptrBase0; // ⛔
              Derived* ptrDerived1 = (Derived*)ptrBase0; // ✅
            
              return 0;
            }
          

4.5 Conversions standard Base* ⟺ Dérivée*

✅ BaseClass* = DerivedClass*;

⛔ DerivedClass* = BaseClass*;


On peut cependant forcer la conversion avec des cast


⚠️ ATTENTION : en pratique, ce genre de conversions est aussi fortement déconseillé car elle induit des memory leaks

4.6 Type statique vs type dynamique

Type statique (static binding)

C'est ainsi que l'objet est déclaré (connu à la compilation)

Type dynamique (dynamic binding)

C'est le type des objets effectivement utilisés / pointés (connu à l'exécution)

4.6 Exemple introductif

Quelques rappels

std::vector<T> : pour stocker une collection d'objets de type T

Range-based for : pour itérer sur une collection



            #include <vector>

            std::vector<T> collection;
            ... 
            // When you want to copy the elements in collection
            for (auto item : collection){item.do();}

            // When you want to modify the elements in the collection
            for (auto &item : collection){item.do();}

            // When you dont want to copy nor modify the elements in the collection
            for (const auto &item : collection){item.do();}
          

4.6 Exemple introductif


                class Animal
                {
                public:
                    virtual ~Animal() = default;
                    std::string id() const { return "animal"; }
                };

                class Cat : public Animal
                {
                public:
                    std::string id() const { return "cat"; }
                };

                class StrayCat : public Cat
                {
                public:
                    std::string id() const { return "stray cat"; }
                };

                class Dog : public Animal
                {
                public:
                    std::string id() const { return "dog"; }
                };
              

4.6 Exemple introductif



                int main()
                {
                  ...

                  std::vector<Animal*> pA = { new Animal, new Dog, new Cat, new StrayCat };
                  for (auto p : pA)
                  {
                      std::println("{}", p->id());
                  }
                
                  return 0;
                }
            

Quelle est la sortie de ce programme ?

4.6 Exemple introductif

La sortie de ce programme est :



            animal
            animal
            animal
            animal
          

Le type statique est Animal * donc on appelle la méthode Animal::id() 4 fois de suite

Le polymorphisme permet de résoudre ce problème et d'accéder automatiquement aux méthodes des classes dérivées en utilisant le type dynamique

4.6 Fonctions virtuelles

C++ peut choisir la méthode à appeler selon la nature de l'objet pointé à l'exécution du programme (type dynamique)

Pour cela, la méthode doit être qualifiée par le mot-clé virtual dans la classe de base

La méthode doit être redéfinie dans les classes dérivées


BONNE PRATIQUE : la méthode redéfinie doit être qualifiée par le mot-clé override dans les classes dérivées


⚠️ ATTENTION : les destructeurs doivent être virtuels pour éviter des fuites de mémoire (destruction partielle)

4.6 Polymorphisme



                class Animal
                {
                public:
                    virtual ~Animal() = default;
                    virtual std::string id() const { return "animal"; }
                };
                class Cat : public Animal
                {
                public:
                    std::string id() const override { return "cat"; }
                };
                class StrayCat : public Cat
                {
                public:
                    std::string id() const override { return "stray cat"; }
                };
                class Dog : public Animal
                {
                public:
                    std::string id() const override { return "dog"; }
                };

Le programme principal (int main(){...} reste le même

4.6 Polymorphisme

La sortie de ce programme est maintenant :



            animal
            dog
            cat
            straycat
          

Le polymorphisme est un point essentiel de la POO (avec l'abstraction de données et l'héritage)

Il permet d'appeler la méthode d'un objet sans se soucier de son type statique, et qu'elle s'adapte au type dynamique

Il est implémenté en C++ par les fonctions virtuelles (virtual)

4.6 Polymorphisme

Le polymorphisme est la faculté pour des objets de différents types (classes) de répondre à des appels de méthodes portant le même nom, chacun correspondant à un code spécifique à chaque type. Le programmeur n'a pas besoin de connaitre a l'avance le type de l'objet, il pourra être déterminé à l'exécution (type dynamique).


Un type (classe) possédant des fonctions virtuelles est nommé type polymorphique.


Pour obtenir un comportement polymorphique en C++, les fonctions membres appelées doivent être virtuelles et les objets doivent être manipulés avec des pointeurs ou des références

4.6 Polymorphisme

Pour que le polymorphisme soit effectif, il faut une fonction f qui soit :

1) une fonction membre d'une classe B

2) redéfinie dans des classes dérivées de B

3) appelée à travers des pointeurs ou des références (sur des objets de B ou de classes dérivées de B)

4) déclarée comme fonction virtuelle (sa déclaration est précédée du mot clé virtual)


Ainsi, si f est appelée à travers un pointeur ou une référence sur un objet de classe C, le choix de la fonction effectivement exécutée (parmi les diverses redéfinitions de f se fera d'après le type dynamique de cet objet.

4.7 Classes abstraites

Parfois, une classe de base est utilisée pour regrouper les caractéristiques communes de plusieurs classes (i.e. Animal)

Certaines méthodes peuvent ne pas être implémentées dans une classe de base, mais doivent l'être dans toutes les classes dérivées. Ces méthodes sont appelées virtuelles pures.

virtual void MakeSound() = 0;

Une classe qui contient au moins une méthode virtuelle pure ne pourra pas être instanciée : c'est une classe abstraite

Une classe abstraite doit être dérivée et ses méthodes virtuelles pures redéfinies dans les classes dérivées.

4.7 Classes Abstraites



                class Animal
                {
                public:
                  virtual ~Animal() = default;
                  virtual void MakeSound() const = 0;
                };
                
                class Cat : public Animal
                {
                public:
                  virtual ~Cat() = default;
                  void MakeSound() const override {...}
                };
                
                class Dog : public Animal
                {
                public:
                  virtual ~Dog() = default;
                  void MakeSound() const override {...}
                };


                int main()
                {
                  // ⛔ Cannot instantiate abstract class
                  auto animal = new Animal;
                  auto cat = new Cat;
                  auto dog = new Dog;
                
                  std::vector<Animal *> animals = {animal, cat, dog};
                
                  for (const auto &animal : animals)
                  {
                    animal->MakeSound();
                  }
                
                  return 0;
                }