1242.2 Langage C++ - 2025-2026
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
Création d'une
La classe dérivée hérite des membres de la classe de base
On peut :
Permet notamment d'éviter la répétition de code
En UML, on va dans le sens de la
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
La classe dérivée est une
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
Exemple de classe de base :
// 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};
};
Exemple de classe dérivée :
// 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};
};
La méthode
Comme la méthode
⇒ Il faut explicitement utiliser
Implémentation de la classe
// 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);
}
Comme l'attribut
Un droit d'accès supplémentaire a été introduit pour pallier ce problème :
Un membre
Le contrôle d'accès
Nouvelle classe de base
// 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};
};
Nouvelle implémentation de la classe
// 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);
}
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.
Dans tous les cas, les membres
| Dérivation | Classe de base | Classe dérivée |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
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
Appel
// Cylinder.cpp
#include "cylinder.h"
Cylinder::Cylinder(double r, double h) : Circle(r)
{
m_h = h;
}
Appel
// 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;
}
On peut faire
Dans ce cas, la classe de base commune à toutes les classes est appelée
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
L'accès aux méthodes de la classe de base dépend du mode de dérivation (
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
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é
Si la classe
⇒ les membres de
⇒ les membres
⇒ toutes les fonctions non
Que peut-on dire de la conversion entre les objets instanciés de ces classes ?
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;
}
cat = dog; // ⛔
dog = cat; // ⛔
animal = cat; // ✅
cat = animal; // ⛔
cat = straycat; // ✅
straycat = cat; // ⛔
animal = straycat; // ✅
On peut cependant rajouter la conversion en implémentant un constructeur de conversion
⇒ les pointeurs de
⇒ les pointeurs de
int main()
{
Base* ptrBase0 = new Base();
Derived* ptrDerived0 = new Derived();
Base *ptrBase1 = ptrDerived0; // ✅
Derived *ptrDerived1 = ptrBase0; // ⛔
Derived* ptrDerived1 = (Derived*)ptrBase0; // ✅
return 0;
}
On peut cependant forcer la conversion avec des
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)
#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();}
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"; }
};
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 ?
La sortie de ce programme est :
animal
animal
animal
animal
Le type statique est
Le
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é
La méthode doit être redéfinie dans les classes dérivées
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 (
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 (
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
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.
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
Une classe qui contient au moins une méthode virtuelle pure ne pourra pas être instanciée : c'est une
Une classe abstraite doit être dérivée et ses méthodes virtuelles pures redéfinies dans les classes dérivées.
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;
}