1. Introduction à l’héritage
L’héritage permet de créer une nouvelle classe à partir d’une classe existante.
La classe existante est appelée classe de base.
La nouvelle classe est appelée classe dérivée.
Exemple :
Code: Select all
#include <iostream>
class Base
{
public:
void hello()
{
std::cout << "Base::hello()\n";
}
};
class Derived : public Base
{
public:
void world()
{
std::cout << "Derived::world()\n";
}
};
int main()
{
Derived d;
d.hello(); // hérité de Base
d.world(); // propre à Derived
}
On parle souvent de relation is-a :
Code: Select all
Derived est un Base
Code: Select all
class Animal {};
class Dog : public Animal {};
À l’inverse, l’héritage n’est pas adapté à une relation de type has-a.
Exemple :
Code: Select all
class Engine {};
class Car
{
Engine engine;
};
2. public, protected et private
Une classe possède trois niveaux d’accès principaux :
Code: Select all
public
protected
private
Accessible depuis n’importe quel code possédant un objet ou une référence valide.
protected
Accessible depuis la classe elle-même et ses classes dérivées.
private
Accessible uniquement depuis la classe qui déclare le membre.
Exemple :
Code: Select all
class Base
{
public:
int publicValue = 10;
protected:
int protectedValue = 20;
private:
int privateValue = 30;
};
class Derived : public Base
{
public:
void test()
{
std::cout << publicValue << '\n'; // OK
std::cout << protectedValue << '\n'; // OK
// std::cout << privateValue; // ERREUR
}
};
La solution habituelle consiste à exposer une fonction dans Base :
Code: Select all
class Base
{
private:
int value = 42;
public:
int getValue() const
{
return value;
}
};
3. Effet du type d’héritage
Avec :
Code: Select all
class Derived : public Base
Avec :
Code: Select all
class Derived : protected Base
Avec :
Code: Select all
class Derived : private Base
Dans la majorité des hiérarchies polymorphiques classiques, on utilise :
Code: Select all
public Base
4. Ajouter du comportement dans une classe dérivée
Une classe dérivée peut conserver les fonctions de Base et ajouter de nouvelles fonctions.
Code: Select all
class Process
{
public:
void start()
{
std::cout << "start\n";
}
};
class DebugProcess : public Process
{
public:
void dumpRegisters()
{
std::cout << "register dump\n";
}
};
Code: Select all
start()
dumpRegisters()
5. Masquage d’une fonction de la classe de base
Une fonction de même nom dans la classe dérivée peut masquer celle de la classe de base.
Code: Select all
class Base
{
public:
void print()
{
std::cout << "Base\n";
}
};
class Derived : public Base
{
public:
void print()
{
std::cout << "Derived\n";
}
};
Code: Select all
Derived d;
d.print();
Code: Select all
Derived
Code: Select all
d.Base::print();
Code: Select all
class Derived : public Base
{
public:
void print()
{
Base::print();
std::cout << "Derived\n";
}
};
6. Le vrai polymorphisme : virtual
Sans virtual, l’appel est généralement déterminé par le type statique de la variable.
Code: Select all
class Base
{
public:
void print()
{
std::cout << "Base\n";
}
};
class Derived : public Base
{
public:
void print()
{
std::cout << "Derived\n";
}
};
Derived d;
Base* p = &d;
p->print();
Pour obtenir un comportement polymorphique :
Code: Select all
class Base
{
public:
virtual void print()
{
std::cout << "Base\n";
}
};
class Derived : public Base
{
public:
void print() override
{
std::cout << "Derived\n";
}
};
Code: Select all
Derived d;
Base* p = &d;
p->print();
Code: Select all
Derived::print()
On distingue donc :
Code: Select all
Type statique : connu à la compilation
Type dynamique : type réel de l’objet à l’exécution
7. override
Le mot-clé override indique au compilateur :
Code: Select all
"Cette fonction doit réellement redéfinir une fonction virtuelle de la base."
Code: Select all
class Base
{
public:
virtual void run(int x);
};
class Derived : public Base
{
public:
void run(int x) override;
};
Code: Select all
void run(double x) override;
Sans override, on risque de créer involontairement une nouvelle fonction au lieu de redéfinir celle de Base.
En pratique moderne :
Code: Select all
virtual dans la classe de base
override dans les classes dérivées
8. final
final peut empêcher l’héritage :
Code: Select all
class Base final
{
};
Code: Select all
class Derived : public Base {};
Code: Select all
class Base
{
public:
virtual void run();
};
class Derived : public Base
{
public:
void run() override final;
};
9. Fonctionnement interne : vptr et vtable
Le standard C++ n’impose pas une implémentation précise des fonctions virtuelles.
Cependant, les compilateurs modernes utilisent généralement une structure conceptuellement proche de :
Code: Select all
Objet
|
+-- vptr ---------> vtable
| +-- adresse fonction virtuelle 1
| +-- adresse fonction virtuelle 2
| +-- adresse destructeur virtuel
|
+-- données
La vtable est une table contenant les adresses des fonctions virtuelles correspondant au type réel de l’objet.
Exemple conceptuel :
Code: Select all
class Base
{
public:
virtual void f();
virtual void g();
};
class Derived : public Base
{
public:
void f() override;
};
Code: Select all
Derived vtable
+----------------------+
| &Derived::f |
| &Base::g |
+----------------------+
10. Coût du polymorphisme dynamique
Une classe polymorphique possède généralement au moins un pointeur supplémentaire par sous-objet polymorphique.
Sur x64, cela représente généralement 8 octets pour un vptr.
Un appel virtuel implique généralement un appel indirect.
Pour du code système, il est utile de savoir que virtual n’est pas "gratuit", mais son coût reste souvent faible comparé au coût d’une abstraction mal conçue.
11. Destructeurs virtuels
Si une classe est destinée à être détruite via un pointeur vers la base, son destructeur doit généralement être virtuel.
Mauvais exemple :
Code: Select all
class Base
{
public:
~Base() {}
};
class Derived : public Base
{
public:
~Derived()
{
std::cout << "cleanup Derived\n";
}
};
Base* p = new Derived;
delete p;
Version correcte :
Code: Select all
class Base
{
public:
virtual ~Base() = default;
};
Code: Select all
Base* p = new Derived;
delete p;
Code: Select all
Derived::~Derived()
Base::~Base()
12. Ordre de construction
Quand on crée un objet dérivé :
Code: Select all
Derived d;
- Base
- membres de Derived, dans leur ordre de déclaration
- corps du constructeur de Derived
Code: Select all
class Base
{
public:
Base()
{
std::cout << "Base\n";
}
};
class Derived : public Base
{
public:
Derived()
{
std::cout << "Derived\n";
}
};
Code: Select all
Base
Derived
13. Ordre de destruction
La destruction se fait en sens inverse :
- corps du destructeur Derived
- membres Derived en ordre inverse
- destructeur Base
14. Appeler un constructeur parent
On utilise la liste d’initialisation :
Code: Select all
class Base
{
public:
explicit Base(int value)
{
std::cout << value << '\n';
}
};
class Derived : public Base
{
public:
Derived(int value)
: Base(value)
{
}
};
15. Ordre réel d’initialisation des membres
L’ordre n’est pas celui écrit dans la liste d’initialisation.
Il dépend de l’ordre de déclaration des membres.
Code: Select all
class Test
{
int a;
int b;
public:
Test()
: b(20), a(10)
{
}
};
Code: Select all
b puis a
Code: Select all
a puis b
16. Constructeurs hérités
On peut importer les constructeurs d’une classe de base :
Code: Select all
class Base
{
public:
Base(int x) {}
Base(double d) {}
};
class Derived : public Base
{
public:
using Base::Base;
};
Code: Select all
Derived a(10);
Derived b(3.14);
Derived peut également définir ses propres constructeurs.
17. Réutilisation d’une implémentation de Base
Une classe dérivée peut redéfinir une fonction tout en réutilisant la logique de la base :
Code: Select all
class Base
{
public:
virtual void initialize()
{
std::cout << "Base init\n";
}
};
class Derived : public Base
{
public:
void initialize() override
{
Base::initialize();
std::cout << "Derived init\n";
}
};
18. Polymorphisme avec références
Le polymorphisme ne nécessite pas forcément des pointeurs.
Code: Select all
void execute(Base& object)
{
object.run();
}
Code: Select all
Derived d;
execute(d);
19. Polymorphisme et collections
On peut stocker des objets différents derrière une interface commune.
Exemple moderne :
Code: Select all
#include <memory>
#include <vector>
std::vector<std::unique_ptr<Base>> objects;
objects.push_back(std::make_unique<DerivedA>());
objects.push_back(std::make_unique<DerivedB>());
for (auto& object : objects)
{
object->run();
}
20. Object slicing
Un piège important :
Code: Select all
Derived d;
Base b = d;
La partie spécifique de Derived est perdue.
C’est ce qu’on appelle :
Code: Select all
object slicing
Code: Select all
Base*
Base&
std::unique_ptr<Base>
std::shared_ptr<Base>
21. Classes abstraites
Une fonction virtuelle pure s’écrit :
Code: Select all
virtual void run() = 0;
Code: Select all
class Device
{
public:
virtual void start() = 0;
virtual ~Device() = default;
};
On ne peut pas écrire :
Code: Select all
Device d;
Code: Select all
class Disk : public Device
{
public:
void start() override
{
std::cout << "Disk start\n";
}
};
22. Une fonction virtuelle pure peut avoir une définition
Il est possible de déclarer :
Code: Select all
class Base
{
public:
virtual void f() = 0;
};
Code: Select all
void Base::f()
{
std::cout << "Base implementation\n";
}
Une classe dérivée peut appeler explicitement :
Code: Select all
Base::f();
23. Upcasting
L’upcast consiste à convertir un objet dérivé vers sa base.
Code: Select all
Derived d;
Base* p = &d;
On peut aussi écrire :
Code: Select all
Base& ref = d;
24. Downcasting
Le downcast fait l’inverse :
Code: Select all
Base* -> Derived*
Exemple :
Code: Select all
Base* base = ...;
Derived* derived = static_cast<Derived*>(base);
Si base ne pointe pas réellement vers un Derived compatible, utiliser le résultat peut provoquer un comportement indéfini.
25. dynamic_cast
dynamic_cast effectue une vérification à l’exécution.
Code: Select all
Base* base = ...;
Derived* derived = dynamic_cast<Derived*>(base);
if (derived)
{
derived->specialFunction();
}
Code: Select all
derived == nullptr
Code: Select all
Derived& d = dynamic_cast<Derived&>(baseRef);
Code: Select all
std::bad_cast
26. RTTI
RTTI signifie :
Code: Select all
Run-Time Type Information
Code: Select all
dynamic_cast
typeid
Code: Select all
#include <typeinfo>
Base& ref = derived;
std::cout << typeid(ref).name();
Le texte produit par name() dépend du compilateur.
27. Éviter d’abuser de typeid
Un code du genre :
Code: Select all
if (typeid(x) == typeid(Dog))
{
...
}
else if (typeid(x) == typeid(Cat))
{
...
}
On préfère souvent :
Code: Select all
virtual void speak() = 0;
Code: Select all
animal.speak();
28. Héritage multiple
C++ permet plusieurs classes de base :
Code: Select all
class Reader
{
public:
void read() {}
};
class Writer
{
public:
void write() {}
};
class Device : public Reader, public Writer
{
};
29. Ambiguïtés de noms
Si deux classes de base possèdent une fonction de même nom :
Code: Select all
class A
{
public:
void run() {}
};
class B
{
public:
void run() {}
};
class C : public A, public B
{
};
Code: Select all
C c;
// c.run(); // ambigu
Code: Select all
c.A::run();
c.B::run();
30. Le problème du diamant
Considérons :
Code: Select all
Animal
/ \
Dog Bird
\ /
DogBird
Code: Select all
class Dog : public Animal {};
class Bird : public Animal {};
class DogBird : public Dog, public Bird {};
Code: Select all
DogBird
+-- Dog
| +-- Animal
|
+-- Bird
+-- Animal
31. Héritage virtuel
On peut partager une seule base commune :
Code: Select all
class Dog : public virtual Animal {};
class Bird : public virtual Animal {};
class DogBird : public Dog, public Bird {};
Code: Select all
Animal
/ \
Dog Bird
\ /
DogBird
L’implémentation mémoire devient cependant plus complexe.
32. Construction d’une base virtuelle
Avec une base virtuelle, c’est la classe la plus dérivée qui est responsable de la construction de la base virtuelle.
Exemple :
Code: Select all
class Animal
{
public:
Animal(int weight) {}
};
class Dog : public virtual Animal
{
public:
Dog(int weight)
: Animal(weight)
{
}
};
class Bird : public virtual Animal
{
public:
Bird(int weight)
: Animal(weight)
{
}
};
class DogBird : public Dog, public Bird
{
public:
DogBird(int weight)
: Animal(weight),
Dog(weight),
Bird(weight)
{
}
};
33. Types de retour covariants
Une fonction virtuelle peut parfois avoir un type de retour plus spécifique dans la dérivée.
Exemple :
Code: Select all
class Base
{
public:
virtual Base* clone();
};
class Derived : public Base
{
public:
Derived* clone() override;
};
Cela fonctionne notamment avec des pointeurs et références de classes liées par héritage.
34. Masquage des overloads
Supposons :
Code: Select all
class Base
{
public:
void f(int);
void f(double);
};
class Derived : public Base
{
public:
void f(const char*);
};
Pour les réintroduire :
Code: Select all
class Derived : public Base
{
public:
using Base::f;
void f(const char*);
};
35. Ajouter une surcharge à une fonction virtuelle
On peut combiner :
Code: Select all
using Base::f;
Exemple :
Code: Select all
class Base
{
public:
virtual void f(int);
};
class Derived : public Base
{
public:
using Base::f;
void f(int) override;
void f(double);
};
36. Fonctions static et héritage
Une fonction membre static ne possède pas de pointeur this.
Elle n’est pas virtuelle.
Exemple :
Code: Select all
class Base
{
public:
static void info()
{
std::cout << "Base\n";
}
};
class Derived : public Base
{
public:
static void info()
{
std::cout << "Derived\n";
}
};
37. Une fonction virtuelle private peut être override
C’est un point subtil.
Code: Select all
class Base
{
private:
virtual void run()
{
std::cout << "Base\n";
}
public:
void execute()
{
run();
}
};
class Derived : public Base
{
private:
void run() override
{
std::cout << "Derived\n";
}
};
Code: Select all
Derived d;
d.execute();
Le fait que Base::run soit private empêche son appel direct depuis Derived, mais n’empêche pas la redéfinition virtuelle.
38. Arguments par défaut et virtual
Voici un piège classique.
Code: Select all
class Base
{
public:
virtual void f(int x = 10)
{
std::cout << "Base " << x << '\n';
}
};
class Derived : public Base
{
public:
void f(int x = 20) override
{
std::cout << "Derived " << x << '\n';
}
};
Code: Select all
Derived d;
Base* p = &d;
p->f();
Code: Select all
Derived::f
Code: Select all
Base*
On obtient conceptuellement :
Code: Select all
Derived 10
39. Changer l’accessibilité d’un membre hérité
using peut changer l’exposition d’un membre hérité dans certains cas.
Exemple :
Code: Select all
class Base
{
protected:
void helper() {}
};
class Derived : public Base
{
public:
using Base::helper;
};
Cela ne change pas la déclaration d’origine dans Base.
40. Constructeur de copie et héritage
Lorsqu’une classe dérivée possède un constructeur de copie personnalisé, il faut aussi copier la partie Base.
Exemple :
Code: Select all
class Base
{
public:
Base(const Base&) = default;
};
class Derived : public Base
{
int value;
public:
Derived(const Derived& other)
: Base(other),
value(other.value)
{
}
};
41. Opérateur d’affectation et héritage
Même principe :
Code: Select all
Derived& operator=(const Derived& other)
{
if (this != &other)
{
Base::operator=(other);
value = other.value;
}
return *this;
}
42. Héritage non public
L’héritage private peut exprimer :
Code: Select all
"Derived est implémenté en utilisant Base"
Code: Select all
"Derived est un Base"
Code: Select all
class Derived : private Base
{
};
Pour une relation "has-a", la composition reste souvent plus claire.
43. static_cast
static_cast sert à de nombreuses conversions vérifiées statiquement.
Exemples :
Code: Select all
double d = 3.14;
int i = static_cast<int>(d);
Code: Select all
Derived d;
Base* b = static_cast<Base*>(&d);
Code: Select all
Derived* d = static_cast<Derived*>(b);
44. dynamic_cast résumé
Utilisation idéale :
Code: Select all
Base* -> Derived*
Code: Select all
if (Derived* d = dynamic_cast<Derived*>(base))
{
d->special();
}
Code: Select all
vérification à l’exécution
Code: Select all
coût RTTI + hiérarchie polymorphique requise
45. reinterpret_cast
reinterpret_cast sert à des conversions très bas niveau.
Exemple :
Code: Select all
std::uintptr_t address =
reinterpret_cast<std::uintptr_t>(ptr);
Code: Select all
void* ptr2 =
reinterpret_cast<void*>(address);
Code: Select all
char* bytes =
reinterpret_cast<char*>(objectPtr);
Il demande essentiellement au compilateur de considérer une valeur sous une autre forme de pointeur ou d’entier autorisée par les règles du langage.
Une mauvaise utilisation peut mener à :
Code: Select all
comportement indéfini
problèmes d’alignement
violation des règles d’aliasing
lecture invalide
crash
46. const_cast
const_cast permet d’ajouter ou retirer const/volatile.
Exemple :
Code: Select all
const int* p = ...;
int* q = const_cast<int*>(p);
Si l’objet original est réellement const, le modifier via q produit un comportement indéfini.
Exemple dangereux :
Code: Select all
const int x = 10;
int* p = const_cast<int*>(&x);
*p = 20;
47. std::bit_cast
std::bit_cast copie la représentation binaire d’un type vers un autre type compatible en taille.
Exemple :
Code: Select all
#include <bit>
#include <cstdint>
float f = 1.0f;
std::uint32_t bits =
std::bit_cast<std::uint32_t>(f);
Les tailles doivent correspondre :
Code: Select all
sizeof(To) == sizeof(From)
Code: Select all
formats binaires
sérialisation contrôlée
analyse de représentation
code bas niveau
48. Comparatif rapide des casts
Code: Select all
static_cast
conversion connue à la compilation
conversions numériques
conversions dans une hiérarchie
pas de contrôle dynamique du downcast
dynamic_cast
vérification runtime
hiérarchie polymorphique
nullptr ou bad_cast en cas d’échec
reinterpret_cast
réinterprétation bas niveau
pointeurs / entiers
très peu de sécurité
const_cast
ajoute ou retire const/volatile
std::bit_cast
copie la représentation binaire
types de même taille
49. Exemple système : interface polymorphique
Même si on préfères généralement une approche procédurale, il est utile de comprendre ce modèle car énormément de code Windows et C++ l’utilise conceptuellement.
Code: Select all
class HandleObject
{
public:
virtual void close() = 0;
virtual ~HandleObject() = default;
};
class FileHandle : public HandleObject
{
public:
void close() override
{
// CloseHandle(...)
}
};
class SocketHandle : public HandleObject
{
public:
void close() override
{
// closesocket(...)
}
};
Code: Select all
void cleanup(HandleObject& object)
{
object.close();
}
50. virtual dans constructeurs et destructeurs
Il faut être prudent avec les fonctions virtuelles appelées pendant la construction ou la destruction.
Pendant le constructeur de Base, la partie Derived n’est pas encore complètement construite.
Le dispatch virtuel ne doit donc pas être imaginé comme si l’objet final Derived était déjà entièrement disponible.
Exemple :
Code: Select all
class Base
{
public:
Base()
{
run();
}
virtual void run()
{
std::cout << "Base\n";
}
};
51. Héritage ou composition ?
Utiliser l’héritage lorsque la relation est réellement :
Code: Select all
Derived est un Base
Code: Select all
Object possède un Component
Code: Select all
class Scheduler
{
};
class ProcessManager
{
Scheduler scheduler;
};
Il en utilise un.
52. Bonnes pratiques modernes
- utiliser override pour chaque override
- destructeur virtuel si destruction polymorphique
- éviter les downcasts quand le design peut être polymorphique
- préférer unique_ptr pour la propriété unique
- éviter les hiérarchies profondes inutilement
- utiliser final lorsque la dérivation n’a pas de sens
- éviter les arguments par défaut différents dans les overrides
- faire attention au slicing
- utiliser dynamic_cast uniquement lorsque réellement nécessaire
- préférer composition à héritage quand la relation n’est pas "is-a"
